diff --git a/app/src/app/(account)/account-settings.tsx b/app/src/app/(account)/account-settings.tsx index 2e5210e..bce3757 100644 --- a/app/src/app/(account)/account-settings.tsx +++ b/app/src/app/(account)/account-settings.tsx @@ -4,16 +4,16 @@ import { useRouter } from "expo-router"; import { useFocusEffect } from "@react-navigation/native"; import { useAccountSettings } from "@hooks/use-account-settings"; +import { useSession } from "@contexts/SessionContext"; import Header from "@components/ui/Header"; import OutlinedTextInput from "@components/ui/OutlinedTextInput"; import PrimaryButton from "@components/ui/PrimaryButton"; export default function AccountSettings() { const router = useRouter(); - const { user, username, setUsername, originalUsername, loading, handleUpdateProfile } = - useAccountSettings(); + const { user, username, setUsername } = useSession(); + const { originalUsername, loading, handleUpdateProfile } = useAccountSettings(); - // navigation function prevCallback() { router.replace("/(tabs)/account"); } @@ -25,10 +25,7 @@ export default function AccountSettings() { return true; }; - const backHandler = BackHandler.addEventListener( - "hardwareBackPress", - backAction, - ); + const backHandler = BackHandler.addEventListener("hardwareBackPress", backAction); return () => backHandler.remove(); }, []), @@ -47,7 +44,7 @@ export default function AccountSettings() { diff --git a/app/src/app/(contribute)/toda-stops.tsx b/app/src/app/(contribute)/toda-stops.tsx index 634307f..d69a99c 100644 --- a/app/src/app/(contribute)/toda-stops.tsx +++ b/app/src/app/(contribute)/toda-stops.tsx @@ -1,5 +1,5 @@ import { router } from "expo-router"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { ActivityIndicator, Alert, SafeAreaView, View, Text, BackHandler } from "react-native"; import { useFocusEffect } from "@react-navigation/native"; @@ -32,6 +32,7 @@ export default function TodaStops() { const [stops, setStops] = useState([]); const [loadingStops, setLoadingStops] = useState(false); + const [formSnapshot, setFormSnapshot] = useState(""); const loadStops = async () => { setLoadingStops(true); @@ -50,15 +51,21 @@ export default function TodaStops() { loadStops(); }, []); - // navigation + // Navigation - const prevCallback = () => { - UnsavedChangesAlert(() => router.replace("/(tabs)/contribute")); - }; + const hasChanges = !!formSnapshot; + + const handleBackPress = useCallback(() => { + if (hasChanges) { + UnsavedChangesAlert(() => router.replace("/(tabs)/contribute")); + } else { + router.replace("/(tabs)/contribute"); + } + }, [hasChanges]); useFocusEffect(() => { const backHandler = BackHandler.addEventListener("hardwareBackPress", () => { - UnsavedChangesAlert(prevCallback); + handleBackPress(); return true; }); return () => backHandler.remove(); @@ -66,7 +73,7 @@ export default function TodaStops() { return ( -
+
@@ -103,7 +110,11 @@ export default function TodaStops() { Loading stops... )} - + setFormSnapshot(JSON.stringify(form))} + /> ); } diff --git a/app/src/app/(search)/2-trip-suggestions.tsx b/app/src/app/(search)/2-trip-suggestions.tsx index d74be41..49d6406 100644 --- a/app/src/app/(search)/2-trip-suggestions.tsx +++ b/app/src/app/(search)/2-trip-suggestions.tsx @@ -71,15 +71,18 @@ export default function SuggestedTrips() { ) : ( {filteredTrips.map((trip) => ( - handleSelectTrip(trip)}> - + handleSelectTrip(trip)} + > + ))} )} - + diff --git a/app/src/app/(tabs)/account.tsx b/app/src/app/(tabs)/account.tsx index 17be758..0fb7e0e 100644 --- a/app/src/app/(tabs)/account.tsx +++ b/app/src/app/(tabs)/account.tsx @@ -1,5 +1,5 @@ -import React, { useEffect } from "react"; -import { Text, SafeAreaView, View, Alert } from "react-native"; +import React from "react"; +import { Text, SafeAreaView, View, Alert, ScrollView } from "react-native"; import { logoutUser } from "@services/account-service"; import { useSession } from "@contexts/SessionContext"; @@ -17,8 +17,8 @@ const tagIcon = require("@assets/option-tag.png"); const todaIcon = require("@assets/transpo-tricycle.png"); export default function Account() { - const { user } = useSession(); - const { username, userRole, points, joinedDate, loading } = useAccountDetails(user?.id); + const { user, username } = useSession(); + const { userRole, points, joinedDate, loading } = useAccountDetails(user?.id); async function handleLogout() { try { @@ -33,14 +33,10 @@ export default function Account() { } function handleLogoutPress() { - Alert.alert( - "Confirm Logout", - "Do you really want to log out?", - [ - { text: "Cancel", style: "cancel" }, - { text: "Log Out", style: "destructive", onPress: () => handleLogout() }, - ] - ); + Alert.alert("Confirm Logout", "Do you really want to log out?", [ + { text: "Cancel", style: "cancel" }, + { text: "Log Out", style: "destructive", onPress: () => handleLogout() }, + ]); } return ( @@ -57,50 +53,51 @@ export default function Account() { joinedDate={joinedDate} /> )} - - - - - My trips - - {userRole === "moderator" && ( - - Moderation + + + + + My trips - )} + {userRole === "moderator" && ( + + Moderation + + )} + + - - + ); } diff --git a/app/src/app/(tabs)/index.tsx b/app/src/app/(tabs)/index.tsx index 99eb695..c79bf89 100644 --- a/app/src/app/(tabs)/index.tsx +++ b/app/src/app/(tabs)/index.tsx @@ -7,14 +7,12 @@ import SymbolSource from "@components/map/SymbolSource"; import RecentTrips from "@components/search/RecentTrips"; import { useSession } from "@contexts/SessionContext"; -import { useAccountDetails } from "@hooks/use-account-details"; import { useMapView } from "@hooks/use-map-view"; import { useLiveUpdates } from "@hooks/use-live-updates"; export default function Index() { - const { user } = useSession(); + const { user, username } = useSession(); if (!user) throw new Error("User must be logged in to view this page."); - const { username } = useAccountDetails(user.id); const router = useRouter(); const { userLocation } = useMapView(); const { symbolRef, updateLiveStatus } = useLiveUpdates("box", 10); diff --git a/app/src/components/contribute/RouteInformation.tsx b/app/src/components/contribute/RouteInformation.tsx index 929f9ad..3090030 100644 --- a/app/src/components/contribute/RouteInformation.tsx +++ b/app/src/components/contribute/RouteInformation.tsx @@ -1,5 +1,6 @@ import React from "react"; import { Text, View } from "react-native"; +import { ScrollView } from "react-native-gesture-handler"; import OutlinedTextInput from "@components/ui/OutlinedTextInput"; import PrimaryButton from "@components/ui/PrimaryButton"; @@ -21,12 +22,11 @@ export default function RouteInformation({ setIsEditingWaypoints, }: RouteInformationProps) { const { route, updateRoute } = useTripCreator(); - const snapPoints = ["48%"]; React.useEffect(() => { - if (route.segmentMode === "Walk" && route.segmentName !== "Walk") { - updateRoute({ segmentName: "Walk" }); - } else if (route.segmentName === "Walk") { + if (route.segmentMode === "Walk" && !route.segmentName.toLowerCase().includes("walk")) { + updateRoute({ segmentName: `Walk from ${route.startLocation} to ${route.endLocation}` }); + } else if (route.segmentName.toLowerCase().includes("walk")) { updateRoute({ segmentName: "" }); } else { updateRoute({ segmentName: route.segmentName }); @@ -43,15 +43,17 @@ export default function RouteInformation({ Route Information - updateRoute({ segmentMode })} - /> + + updateRoute({ segmentMode })} + /> + updateRoute({ segmentName })} diff --git a/app/src/components/contribute/TodaInformation.tsx b/app/src/components/contribute/TodaInformation.tsx index 27e9def..b541ffb 100644 --- a/app/src/components/contribute/TodaInformation.tsx +++ b/app/src/components/contribute/TodaInformation.tsx @@ -33,21 +33,25 @@ const TODA_COLORS = [ "Violet", "Black", "White", + "Pink", "None", ]; interface TodaStopsProps { coordinates: Coordinates | null; onNewStopAdded: () => void; + onFormChange?: (form: { todaName: string; color: string; landmark: string }) => void; } -export default function TodaStops({ coordinates, onNewStopAdded }: TodaStopsProps) { +export default function TodaStops({ coordinates, onNewStopAdded, onFormChange }: TodaStopsProps) { const { user } = useSession(); const [form, setForm] = useState({ todaName: "", color: "", landmark: "" }); const [dialogVisible, setDialogVisible] = useState(false); const updateForm = (key: keyof typeof form, value: string) => { - setForm((prev) => ({ ...prev, [key]: value })); + const newForm = { ...form, [key]: value }; + setForm(newForm); + onFormChange?.(newForm); }; const resetForm = () => { diff --git a/app/src/components/contribute/TransportModeInput.tsx b/app/src/components/contribute/TransportModeInput.tsx index 6860f80..75a5ea2 100644 --- a/app/src/components/contribute/TransportModeInput.tsx +++ b/app/src/components/contribute/TransportModeInput.tsx @@ -16,7 +16,7 @@ export default function TransportModeInput({ value, onChange }: Props) { }; return ( - + {TRANSPORTATION_MODES.map((mode) => ( filters.transportModes.includes(m)); - const isUnchanged = isSortSame && areModesSame; + const isTimeSame = timeToLeave.getTime() === filters.timeToLeave.getTime(); + + const isUnchanged = isSortSame && areModesSame && isTimeSame; if (!isUnchanged) { - applyFilters({ sortBy, transportModes: selectedModes }); + applyFilters({ timeToLeave, sortBy, transportModes: selectedModes }); } sheetRef.current?.close(); }; @@ -51,7 +53,7 @@ export default function FilterSearch({ sheetRef, filters, applyFilters }: Props) > - Filter trips + Filter & sort trips diff --git a/app/src/components/search/TripSummary.tsx b/app/src/components/search/TripSummary.tsx index 3edd018..cf590bc 100644 --- a/app/src/components/search/TripSummary.tsx +++ b/app/src/components/search/TripSummary.tsx @@ -66,7 +66,10 @@ export default function TripSummary({ {currentUserId && } handleCommentPress(trip.id)} hitSlop={10}> - + + + {trip.comments} + diff --git a/app/src/components/ui/TripPreview.tsx b/app/src/components/ui/TripPreview.tsx index 7df331d..d375ecd 100644 --- a/app/src/components/ui/TripPreview.tsx +++ b/app/src/components/ui/TripPreview.tsx @@ -13,16 +13,20 @@ const comment = require("@assets/social-comment.png"); const bookmarkIcon = require("@assets/social-bookmark.png"); const bookmarkedIcon = require("@assets/social-bookmarked.png"); -export default function TripPreview({ trip }: { trip: FullTrip }) { +export default function TripPreview({ + trip, + timeToLeave = new Date(), +}: { + trip: FullTrip; + timeToLeave?: Date; +}) { const { user } = useSession(); const { bookmarked, toggleBookmark } = useTripPreviewData(user?.id || null, trip); const totalDuration = trip.segments.reduce((sum, seg) => sum + seg.duration, 0); const totalCost = trip.segments.reduce((sum, seg) => sum + seg.cost, 0); const transportModes = trip.segments.map((seg) => seg.segmentMode); - - const currentTime = new Date(); - const arrivalTime = new Date(currentTime.getTime() + totalDuration * 1000); + const arrivalTime = new Date(timeToLeave.getTime() + totalDuration * 1000); const arrivalTimeString = arrivalTime.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", diff --git a/app/src/contexts/SessionContext.tsx b/app/src/contexts/SessionContext.tsx index 608d2e4..567d33e 100644 --- a/app/src/contexts/SessionContext.tsx +++ b/app/src/contexts/SessionContext.tsx @@ -5,24 +5,47 @@ import { User } from "@supabase/supabase-js"; interface SessionContextType { user: User | null; + username: string | null; + setUsername: (newUsername: string) => void; } const SessionContext = createContext(undefined); export function SessionProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); + const [username, setUsername] = useState(null); const [loading, setLoading] = useState(true); + async function handleUserSession(sessionUser: User | null) { + setUser(sessionUser); + + if (sessionUser) { + const { data } = await supabase + .from("profiles") + .select("username") + .eq("id", sessionUser.id) + .single(); + setUsername(data?.username ?? null); + } else { + setUsername(null); + } + } + useEffect(() => { - // Fetch initial session - setLoading(true); - supabase.auth.getSession().then(({ data: { session } }) => { + async function initSession() { + setLoading(true); + const { + data: { session }, + } = await supabase.auth.getSession(); + await handleUserSession(session?.user ?? null); setLoading(false); - setUser(session?.user ?? null); - }); + } + + initSession(); - const { data: authListener } = supabase.auth.onAuthStateChange((_event, session) => { - setUser(session?.user ?? null); + // Listen for auth state changes + const { data: authListener } = supabase.auth.onAuthStateChange(async (_event, session) => { + await handleUserSession(session?.user ?? null); }); return () => { @@ -31,7 +54,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { }, []); return ( - + {loading ? : children} ); diff --git a/app/src/contexts/TripSearchContext.tsx b/app/src/contexts/TripSearchContext.tsx index bed9da9..c797533 100644 --- a/app/src/contexts/TripSearchContext.tsx +++ b/app/src/contexts/TripSearchContext.tsx @@ -7,6 +7,7 @@ import { useSession } from "@contexts/SessionContext"; import { fetchTripData } from "@services/trip-service"; const FILTER_INITIAL_STATE = { + timeToLeave: new Date(), sortBy: "Verified by moderators", transportModes: ["Train", "Bus", "Jeep", "UV", "Tricycle"], }; @@ -53,7 +54,7 @@ export function TripSearchProvider({ children }: { children: ReactNode }) { } }; - const applyFilters = ({ sortBy, transportModes }: FilterState) => { + const applyFilters = ({ timeToLeave, sortBy, transportModes }: FilterState) => { const filtered = suggestedTrips .filter((trip) => trip.segments.every( @@ -62,7 +63,7 @@ export function TripSearchProvider({ children }: { children: ReactNode }) { ) .sort(getSortFunction(sortBy)); - setFilters({ sortBy, transportModes }); + setFilters({ timeToLeave, sortBy, transportModes }); setFilteredTrips(filtered); }; diff --git a/app/src/hooks/use-account-details.tsx b/app/src/hooks/use-account-details.tsx index 065da8a..5b20c76 100644 --- a/app/src/hooks/use-account-details.tsx +++ b/app/src/hooks/use-account-details.tsx @@ -1,13 +1,7 @@ import { useEffect, useState } from "react"; -import { - getUsername, - getUserRole, - getUserPoints, - getUserJoinedDate, -} from "@services/account-service"; +import { getUserRole, getUserPoints, getUserJoinedDate } from "@services/account-service"; export interface AccountDetails { - username: string; userRole: string; points: number; joinedDate: string | null; @@ -15,7 +9,6 @@ export interface AccountDetails { } export function useAccountDetails(userId: string | undefined): AccountDetails { - const [username, setUsername] = useState("Unknown user"); const [userRole, setUserRole] = useState("Commuter"); const [points, setPoints] = useState(0); const [joinedDate, setJoinedDate] = useState(null); @@ -25,14 +18,12 @@ export function useAccountDetails(userId: string | undefined): AccountDetails { async function fetchUserDetails() { if (userId) { setLoading(true); - const [fetchedRole, fetchedUsername, fetchedPoints, fetchedJoinedDate] = await Promise.all([ + const [fetchedRole, fetchedPoints, fetchedJoinedDate] = await Promise.all([ getUserRole(userId), - getUsername(userId), getUserPoints(userId), getUserJoinedDate(userId), ]); setUserRole(fetchedRole || "Unknown user"); - setUsername(fetchedUsername || "Commuter"); setPoints(fetchedPoints); setJoinedDate(fetchedJoinedDate); } @@ -41,5 +32,5 @@ export function useAccountDetails(userId: string | undefined): AccountDetails { fetchUserDetails(); }, [userId]); - return { username, userRole, points, joinedDate, loading }; + return { userRole, points, joinedDate, loading }; } diff --git a/app/src/types/global.d.ts b/app/src/types/global.d.ts index 3c3b915..1197c05 100644 --- a/app/src/types/global.d.ts +++ b/app/src/types/global.d.ts @@ -7,6 +7,7 @@ declare global { }; type FilterState = { + timeToLeave: Date; sortBy: string; transportModes: string[]; };