Fix Supabase OAuth flow and various UI updates

This commit is contained in:
RoadtripDJ Dev
2026-05-17 20:48:19 +01:00
parent ad64bb2572
commit dedf25c51f
7 changed files with 326 additions and 57 deletions

View File

@@ -1,27 +1,191 @@
import React from 'react';
import { View, Text, StyleSheet, SafeAreaView, TouchableOpacity, TextInput, ImageBackground, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import { X, MapPin, ArrowRight } from 'lucide-react-native';
import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, KeyboardAvoidingView, ScrollView, Platform, ActivityIndicator, Alert, Linking, StatusBar } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { X, MapPin, ArrowRight, Navigation } from 'lucide-react-native';
import { colors } from '../../utils/colors';
import { supabase } from '../../services/supabase';
// @ts-ignore
export default function NewTripScreen({ navigation }) {
const [tripName, setTripName] = useState('');
const [origin, setOrigin] = useState('');
const [destination, setDestination] = useState('');
const [distance, setDistance] = useState('');
const [duration, setDuration] = useState('');
const [loading, setLoading] = useState(false);
const handleCreateRoute = () => {
// In a real app, this would trigger the orchestration flow (Ollama -> Spotify -> Firebase)
// For now, we mock success and navigate to the Trip Details
navigation.replace('TripDetails');
const handleCalculateTrip = async () => {
if (!origin || !destination) {
Alert.alert('Erro', 'Por favor preenche ambos os campos de partida e destino.');
return;
}
if (!tripName) {
Alert.alert('Erro', 'Por favor dá um nome à tua viagem.');
return;
}
setLoading(true);
try {
const apiKey = 'AIzaSyDocu-PEHAyrdV8OUEPMXye9A_rpYzOA34';
const url = `https://maps.googleapis.com/maps/api/directions/json?origin=${encodeURIComponent(origin)}&destination=${encodeURIComponent(destination)}&key=${apiKey}`;
const response = await fetch(url);
const data = await response.json();
if (data.status === 'OK') {
const leg = data.routes[0].legs[0];
const finalDistance = leg.distance.text;
const finalDuration = leg.duration.text;
const tripDurationMs = leg.duration.value * 1000;
setDistance(finalDistance);
setDuration(finalDuration);
try {
// A. Get provider token
const { data: sessionData, error: sessionError } = await supabase.auth.getSession();
if (sessionError || !sessionData?.session) throw new Error('Session not found.');
const providerToken = sessionData.session.provider_token;
if (!providerToken) throw new Error('Spotify provider token missing.');
// B. Fetch Spotify User ID
const spotifyUserRes = await fetch('https://api.spotify.com/v1/me', {
headers: { 'Authorization': `Bearer ${providerToken}` }
});
const spotifyUserData = await spotifyUserRes.json();
if (!spotifyUserData.id) throw new Error('Could not fetch Spotify User ID');
const spotifyUserId = spotifyUserData.id;
// C. Call Ollama server
const ollamaRes = await fetch("http://89.114.196.110:11434/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "qwen3-coder:30b",
messages: [{ "role": "user", "content": `I am taking a roadtrip from ${origin} to ${destination}. Reply ONLY with 3 Spotify genre seeds separated by commas (e.g., pop,rock,indie) that fit this journey. No other text.` }],
stream: false
})
});
const ollamaData = await ollamaRes.json();
const seed_genres = ollamaData.message.content.trim().replace(/\s+/g, '').toLowerCase();
// D. Create empty playlist
const createPlaylistRes = await fetch(`https://api.spotify.com/v1/users/${spotifyUserId}/playlists`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${providerToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: tripName,
description: `Roadtrip from ${origin} to ${destination}. Genres: ${seed_genres}`,
public: false
})
});
const playlistData = await createPlaylistRes.json();
if (!playlistData.id) throw new Error('Could not create playlist');
const playlistId = playlistData.id;
const generatedPlaylistUrl = playlistData.external_urls.spotify;
// E. Fetch Spotify track recommendations
let accumulatedDurationMs = 0;
let trackUris: string[] = [];
let attempts = 0;
while (accumulatedDurationMs < tripDurationMs && attempts < 10) {
const recommendationsRes = await fetch(`https://api.spotify.com/v1/recommendations?seed_genres=${encodeURIComponent(seed_genres)}&limit=50`, {
headers: { 'Authorization': `Bearer ${providerToken}` }
});
if (!recommendationsRes.ok) break;
const recommendationsData = await recommendationsRes.json();
if (!recommendationsData.tracks || recommendationsData.tracks.length === 0) break;
for (const track of recommendationsData.tracks) {
if (!trackUris.includes(track.uri)) {
trackUris.push(track.uri);
accumulatedDurationMs += track.duration_ms;
if (accumulatedDurationMs >= tripDurationMs) break;
}
}
attempts++;
}
if (trackUris.length > 0) {
// F. Add tracks to playlist
const chunkSize = 100;
for (let i = 0; i < trackUris.length; i += chunkSize) {
const chunk = trackUris.slice(i, i + chunkSize);
await fetch(`https://api.spotify.com/v1/playlists/${playlistId}/tracks`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${providerToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ uris: chunk })
});
}
} else {
console.log("No tracks found for genres:", seed_genres);
}
// G. Save to Supabase
const { data: { user } } = await supabase.auth.getUser();
if (user) {
const { error: dbError } = await supabase.from('trips').insert({
user_id: user.id,
title: tripName,
origin,
destination,
distance: finalDistance,
duration: finalDuration,
playlist_url: generatedPlaylistUrl
});
if (dbError) {
console.log("DB Insert error:", dbError);
}
}
Alert.alert('Sucesso!', 'Viagem e Playlist Criadas!');
} catch (playlistError) {
console.log("Error generating playlist:", playlistError);
Alert.alert('Erro Playlist', 'A viagem foi calculada, mas ocorreu um erro a criar a playlist.');
}
} else {
// O NOSSO DETETIVE ENTRA AQUI!
console.log("ERRO DA GOOGLE:", data);
Alert.alert(
'Culpado Encontrado',
`Motivo: ${data.status}\nDetalhe: ${data.error_message || 'Vê o terminal preto do PC'}`
);
}
} catch (error) {
Alert.alert('Erro', 'Ocorreu um erro ao comunicar com a API da Google.');
console.log("Erro de código:", error);
} finally {
setLoading(false);
}
};
const handleOpenGoogleMaps = () => {
if (!origin || !destination) {
Alert.alert('Erro', 'Por favor preenche ambos os campos de partida e destino.');
return;
}
const url = `https://www.google.com/maps/dir/?api=1&origin=${encodeURIComponent(origin)}&destination=${encodeURIComponent(destination)}`;
Linking.openURL(url);
};
return (
<SafeAreaView style={styles.safeArea}>
<KeyboardAvoidingView
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>Nova Viagem</Text>
<TouchableOpacity
<TouchableOpacity
style={styles.closeButton}
onPress={() => navigation.goBack()}
>
@@ -34,23 +198,25 @@ export default function NewTripScreen({ navigation }) {
<View style={styles.mapArea}>
{/* Using a solid light gray color instead of a complex map image to keep it clean */}
<View style={styles.mockRouteVisual}>
<View style={styles.routeDotLarge} />
<View style={styles.routeLineDashed} />
<View style={styles.routePinLarge}>
<MapPin color={colors.white} size={14} />
</View>
<View style={styles.routeDotLarge} />
<View style={styles.routeLineDashed} />
<View style={styles.routePinLarge}>
<MapPin color={colors.white} size={14} />
</View>
</View>
</View>
{/* Form Card */}
<View style={styles.formCard}>
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>NOME DA VIAGEM</Text>
<TextInput
<TextInput
style={styles.textInput}
placeholder="Ex: Fim de semana no Algarve"
placeholderTextColor={colors.textSecondary}
value={tripName}
onChangeText={setTripName}
/>
</View>
@@ -65,36 +231,72 @@ export default function NewTripScreen({ navigation }) {
<View style={styles.routeInputs}>
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>PARTIDA</Text>
<TextInput
<TextInput
style={[styles.textInput, styles.routeTextInput]}
value="Lisboa, Portugal"
placeholderTextColor={colors.textMain}
placeholder="Ex: Lisboa, Portugal"
placeholderTextColor={colors.textSecondary}
value={origin}
onChangeText={setOrigin}
/>
</View>
<View style={[styles.inputGroup, { marginBottom: 0 }]}>
<Text style={styles.inputLabel}>DESTINO</Text>
<TextInput
<TextInput
style={[styles.textInput, styles.routeTextInput]}
value="Porto, Portugal"
placeholderTextColor={colors.textMain}
placeholder="Ex: Porto, Portugal"
placeholderTextColor={colors.textSecondary}
value={destination}
onChangeText={setDestination}
/>
</View>
</View>
</View>
{/* Results Section */}
{(distance || duration) ? (
<View style={styles.resultsContainer}>
<View style={styles.resultItem}>
<Text style={styles.resultLabel}>Distância</Text>
<Text style={styles.resultValue}>{distance}</Text>
</View>
<View style={styles.resultDivider} />
<View style={styles.resultItem}>
<Text style={styles.resultLabel}>Duração</Text>
<Text style={styles.resultValue}>{duration}</Text>
</View>
</View>
) : null}
</View>
{/* Bottom Actions */}
<View style={styles.bottomActions}>
<TouchableOpacity
<TouchableOpacity
style={styles.primaryButton}
onPress={handleCreateRoute}
onPress={handleCalculateTrip}
disabled={loading}
>
<Text style={styles.primaryButtonText}>Criar Rota & Playlist</Text>
<ArrowRight color={colors.white} size={20} />
{loading ? (
<ActivityIndicator color={colors.white} />
) : (
<>
<Text style={styles.primaryButtonText}>Calcular Viagem</Text>
<ArrowRight color={colors.white} size={20} />
</>
)}
</TouchableOpacity>
{(distance || duration) ? (
<TouchableOpacity
style={styles.secondaryButton}
onPress={handleOpenGoogleMaps}
>
<Navigation color={colors.primary} size={20} />
<Text style={styles.secondaryButtonText}>Abrir no Google Maps</Text>
</TouchableOpacity>
) : null}
<Text style={styles.disclaimerText}>
A IA vai analisar o trajeto, pontos de interesse, clima{'\n'}e duração para criar a banda sonora perfeita.
</Text>
@@ -282,4 +484,46 @@ const styles = StyleSheet.create({
lineHeight: 18,
fontWeight: '500',
},
resultsContainer: {
flexDirection: 'row',
marginTop: 24,
paddingTop: 24,
borderTopWidth: 1,
borderTopColor: colors.inputBorder,
},
resultItem: {
flex: 1,
alignItems: 'center',
},
resultDivider: {
width: 1,
backgroundColor: colors.inputBorder,
marginHorizontal: 16,
},
resultLabel: {
fontSize: 12,
color: colors.textSecondary,
marginBottom: 4,
fontWeight: 'bold',
},
resultValue: {
fontSize: 18,
color: colors.textMain,
fontWeight: 'bold',
},
secondaryButton: {
backgroundColor: colors.inputBackground,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 18,
borderRadius: 16,
marginBottom: 16,
},
secondaryButtonText: {
color: colors.primary,
fontSize: 16,
fontWeight: 'bold',
marginLeft: 8,
},
});