diff --git a/TripPlanner.tsx b/TripPlanner.tsx deleted file mode 100644 index fabeeaf..0000000 --- a/TripPlanner.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import React, { useState } from "react"; -import { - Flex, - ComboBox, - Item, - View, - Button, - Heading, - Divider, - Text, - useAsyncList, - Tabs, - TabList, - TabPanels, - Content -} from '@adobe/react-spectrum'; - -interface Place { - type: string; - name: string; - id: string; - lat: number; - lon: number; -} - -interface Trip { - id: string; - duration: string; - segments: Array<{ - mode: string; - from: string; - to: string; - start_time: string; - end_time: string; - }>; -} - -const usePlaceSuggestions = () => { - return useAsyncList({ - async load({ signal, filterText }) { - if (!filterText) return { items: [] }; - const response: Response = await fetch(`http://motis.metroll.live/api/v1/geocode?text=${filterText}`, { signal }); - const data = await response.json(); - const places = data.map((pl: Place) => ({ - type: pl.type, - name: pl.name, - id: pl.id || null, - lat: pl.lat, - lon: pl.lon - })); - return { items: places.slice(0, 5) }; - } - }); -}; - -const getTripSuggestions = async (fromPlace: string, toPlace: string): Promise => { - const currentTime = new Date().toISOString(); - const response = await fetch(`http://motis.metroll.live/api/v1/plan?time=${currentTime}&fromPlace=${fromPlace}&toPlace=${toPlace}&arriveBy=false`); - - if (!response.ok) { - console.error("Error fetching trip data:", response.status); - return []; - } - - const data = await response.json(); - if (!data.itineraries || data.itineraries.length === 0) { - return []; - } - - return data.itineraries.slice(0, 5).map((itinerary: any) => ({ - id: itinerary.id || "N/A", - duration: itinerary.duration || "N/A", - segments: itinerary.legs.map((leg: any) => ({ - mode: leg.mode, - from: leg.from.name || "N/A", - to: leg.to.name || "N/A", - start_time: leg.startTime || "N/A", - end_time: leg.endTime || "N/A" - })) - })); -}; - -const TripPlanner: React.FC = () => { - const [fromPlace, setFromPlace] = useState(null); - const [toPlace, setToPlace] = useState(null); - const [trips, setTrips] = useState([]); - const [selectedTripIndex, setSelectedTripIndex] = useState(0); - const [error, setError] = useState(null); - - const fromPlaceSuggestions = usePlaceSuggestions(); - const toPlaceSuggestions = usePlaceSuggestions(); - - const handlePlanTripButton = async () => { - setError(null); - - if (!fromPlace && !toPlace) { - setError("Both origin and destination are required."); - } else if (!fromPlace) { - setError("Origin is required."); - } else if (!toPlace) { - setError("Destination is required."); - } else { - const tripData = await getTripSuggestions(fromPlace.id, toPlace.id); - if (tripData.length > 0) { - setTrips(tripData); - setSelectedTripIndex(0); - } else { - setTrips([]); - setError("No trip found. Please try different locations."); - } - } - - - - - }; - - const formatDuration = (seconds: number): string => { - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - let formattedDuration = ""; - - if (hours > 0) { - formattedDuration += `${hours} hour${hours > 1 ? 's' : ''}`; - } - - if (minutes > 0) { - if (hours > 0) formattedDuration += " "; - formattedDuration += `${minutes} minute${minutes > 1 ? 's' : ''}`; - } - - return formattedDuration || "0 minutes"; - }; - - return ( - - - Trip Planner - - - - { - const selectedItem = fromPlaceSuggestions.items.find(item => item.id === key); - setFromPlace(selectedItem || null); - }} - loadingState={fromPlaceSuggestions.loadingState} - placeholder="Select origin" - width="100%" - > - {(item) => {item.name}} - - - { - const selectedItem = toPlaceSuggestions.items.find(item => item.id === key); - setToPlace(selectedItem || null); - }} - loadingState={toPlaceSuggestions.loadingState} - placeholder="Select destination" - width="100%" - > - {(item) => {item.name}} - - - - - - - - {error ? ( - Error: {error} - ) : trips.length > 0 ? ( - setSelectedTripIndex(Number(key))}> - - {trips.map((_, index) => ( - Trip {index + 1} - ))} - - - {trips.map((trip, index) => ( - - - Trip Details - Duration: {formatDuration(Number(trip.duration))} - - {trip.segments.map((segment, segIndex) => ( - - - {segIndex === 0 ? "START: " : ""}{segment.mode}{segIndex === trip.segments.length - 1 ? " END" : ""} - {new Date(segment.start_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - -
{segment.from}
to
{segment.to}
- -
- ))} -
-
- ))} -
-
- ) : ( - Enter an Origin and Destination to receive a planned trip. - )} -
-
- ); -}; - -export default TripPlanner; diff --git a/src/components/TripPlanner.tsx b/src/components/TripPlanner.tsx index dc85d4c..2b53980 100644 --- a/src/components/TripPlanner.tsx +++ b/src/components/TripPlanner.tsx @@ -1,6 +1,19 @@ // Worked on by Yoel and Nishil import React, {useState} from "react"; -import {Flex, ComboBox, Item, View, Button, useAsyncList, ListBox, Text} from '@adobe/react-spectrum'; +import { + Flex, + ComboBox, + Item, + View, + Button, + useAsyncList, + ListBox, + Text, + Tabs, + TabList, + TabPanels, Content, Heading, Divider +} from '@adobe/react-spectrum'; +import {wait} from "@testing-library/user-event/dist/utils"; interface Place { type: string, @@ -11,6 +24,7 @@ interface Place { } interface Trip { + id: string; duration: number; // Duration in seconds startTime: string; // ISO timestamp of start time endTime: string; // ISO timestamp of end time @@ -53,7 +67,7 @@ const usePlaceSuggestions = () => { }); }; -const getTripSuggestions = async (fromPlace: Place, toPlace: Place) => { +const getTripSuggestions = async (fromPlace: Place, toPlace: Place)=> { if (!fromPlace || !toPlace) return { items: [] }; const currentTime = new Date().toISOString(); let fromPlaceId = fromPlace.id; @@ -66,101 +80,173 @@ const getTripSuggestions = async (fromPlace: Place, toPlace: Place) => { const data = await response.json(); let trips = data.itineraries.map((trip: Trip) => ({ ...trip, + id: `${trip.startTime}-${trip.endTime}-${trip.transfers}`, legs: trip.legs.map((leg: Leg) => ({ ...leg, + routeShortName: leg.mode === "WALK" ? "Walk" : leg.routeShortName })) + })); return {items: trips.slice(0, 5)}; }; +const formatDuration = (seconds: number): string => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + let formattedDuration = ""; + + if (hours > 0) { + formattedDuration += `${hours} hour${hours > 1 ? 's' : ''}`; + } + + if (minutes > 0) { + if (hours > 0) formattedDuration += " "; + formattedDuration += `${minutes} minute${minutes > 1 ? 's' : ''}`; + } + + return formattedDuration || "0 minutes"; +}; + + const TripPlanner: React.FC = () => { const [fromPlace, setFromPlace] = useState(null); const [toPlace, setToPlace] = useState(null); const [trips, setTrips] = useState([]); const [showTrips, setShowTrips] = useState(false); + const [tripDetails, setTripDetails] = useState(null); + const [showTripDetails, setShowTripDetails] = useState(false); const [error, setError] = useState(null); + const [isLoading, setIsLoading] = React.useState(false); const fromPlaceSuggestions = usePlaceSuggestions(); const toPlaceSuggestions = usePlaceSuggestions(); const HandlePlanTripButton = async () => { + setShowTrips(false); + setShowTripDetails(false); if (fromPlace && toPlace) { - const result = await getTripSuggestions(fromPlace, toPlace); - if (result.items.length === 0) { - setError("No trips found."); - } else { - setTrips(result.items); - setError(null); + if (fromPlace.id === toPlace.id) { + setError("Origin and destination cannot be the same."); + } + else { + setIsLoading(true); + await wait(1000); + const result = await getTripSuggestions(fromPlace, toPlace); + if (result.items.length === 0) { + setError("No trips found."); + } else { + setError(null); + setTrips(result.items); + + } } } else { setError("Please fill in both the origin and destination fields."); } + setIsLoading(false); setShowTrips(true); }; return ( - - - - {/* Start Location ComboBox */} - { - const selectedItem = fromPlaceSuggestions.items.find(item => item.id === key); - setFromPlace(selectedItem || null); - }} - loadingState={fromPlaceSuggestions.loadingState} - direction="bottom" - shouldFlip={true} - menuTrigger="input" - width="100%" - placeholder="Origin " - > - {(item) => {item.name}} - - {/* End Location ComboBox */} - { - const selectedItem = toPlaceSuggestions.items.find(item => item.id === key); - setToPlace(selectedItem || null); - }} - loadingState={toPlaceSuggestions.loadingState} - direction="bottom" - shouldFlip={true} - menuTrigger="input" - width="100%" - placeholder="Destination " - > - {(item) => {item.name}} - - {/* Plan Trip Button */} - - - - {/* Display Trip Suggestions */} - {showTrips && ( - - {error ? ( - {error} - ) : ( - + + + + + {/* Start Location ComboBox */} + { + const selectedItem = fromPlaceSuggestions.items.find(item => item.id === key); + setFromPlace(selectedItem || null); + }} + loadingState={fromPlaceSuggestions.loadingState} + direction="bottom" + shouldFlip={true} + menuTrigger="input" + width="100%" + + > + {(item) => {item.name}} + + {/* End Location ComboBox */} + { + const selectedItem = toPlaceSuggestions.items.find(item => item.id === key); + setToPlace(selectedItem || null); + }} + loadingState={toPlaceSuggestions.loadingState} + direction="bottom" + shouldFlip={true} + menuTrigger="input" + width="100%" + > + {(item) => {item.name}} + + {/* Plan Trip Button */} + + + + + {/* Display Trip Suggestions */} + {showTrips && ( + + {error ? ( + {error} + ) : ( + { + let selectedKey = Array.from(key)[0]; + const selectedTrip = trips.find(trip => trip.id === selectedKey); + setTripDetails(selectedTrip || null); + setShowTripDetails(true); + + }} + + > {(item) => ( - - {item.legs.map(leg => leg.mode === "WALK" ? "Walk" : leg.routeShortName).join(' • ')} + + {item.legs.map(leg => leg.routeShortName).join(' • ')} {item.duration / 60} min )} - - )} - - )} + + )} + + )} + + + {/* Display Trip Details */} + {showTripDetails && ( + + + {fromPlace?.name} - {toPlace?.name} + Duration: {formatDuration(Number(tripDetails?.duration))} + + {tripDetails?.legs.map((leg: Leg, legIndex: number) => ( + + + {tripDetails.legs.length > 1 && (legIndex === 0 ? "START: " : legIndex === tripDetails.legs.length - 1 ? " END: " : "")}{leg.routeShortName} +
{new Date(leg.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} to {new Date(leg.endTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +
{leg.from.name} to {leg.to.name} +
+ +
+ ))} +
+
+ )}; +
); }; diff --git a/src/components/TripPlannerOld.tsx b/src/components/TripPlannerOld.tsx deleted file mode 100644 index 75b290c..0000000 --- a/src/components/TripPlannerOld.tsx +++ /dev/null @@ -1,186 +0,0 @@ -// Worked on by Yoel and Nishil - -import React, { useState, useEffect, useRef } from 'react'; -import { Flex, TextField, Button, View, Heading } from '@adobe/react-spectrum'; -import './TripPlanner.css'; // Import custom CSS for styling - -interface SuggestionProps { - name: string; - lat: number; - lon: number; - id: string; -} -const TripPlannerOld: React.FC = () => { - - - const [startLocation, setStartLocation] = useState(''); - const [endLocation, setEndLocation] = useState(''); - const [tripDetails, setTripDetails] = useState(null); - const [startSuggestions, setStartSuggestions] = useState([]); - const [endSuggestions, setEndSuggestions] = useState([]); - const [startDetails, setStartDetails] = useState<{ lat: number; lon: number; id: string } | null>(null); - const [endDetails, setEndDetails] = useState<{ lat: number; lon: number; id: string } | null>(null); - - // Refs for suggestion containers - const startSuggestionsRef = useRef(null); - const endSuggestionsRef = useRef(null); - - // Fetch location suggestions (Station Guesser) - const fetchLocationSuggestions = async (input: string) => { - const response = await fetch(`http://motis.metroll.live/api/v1/geocode?text=${input}`); - const data = await response.json(); - return data.map((loc: { name: string; lat: number; lon: number; id: string }) => ({ - name: loc.name, - lat: loc.lat, - lon: loc.lon, - id: loc.id, - })); - }; - - // Effect to fetch suggestions for start location - useEffect(() => { - if (startLocation) { - const getSuggestions = async () => { - const suggestions = await fetchLocationSuggestions(startLocation); - setStartSuggestions(suggestions); - }; - getSuggestions(); - } else { - setStartSuggestions([]); // Clear suggestions if input is empty - } - }, [startLocation]); - - // Effect to fetch suggestions for end location - useEffect(() => { - if (endLocation) { - const getSuggestions = async () => { - const suggestions = await fetchLocationSuggestions(endLocation); - setEndSuggestions(suggestions); - }; - getSuggestions(); - } else { - setEndSuggestions([]); // Clear suggestions if input is empty - } - }, [endLocation]); - - // Fetch trip itinerary using MOTIS - const fetchTripItinerary = async () => { - - if (!startDetails?.id || !endDetails?.id) { - alert('Please select a valid start and end location'); - return; - } - - // Fetch trip details if ids are valid - const response = await fetch( - `http://motis.metroll.live/api/v1/plan?time=2024-10-28T01%3A22%3A00.000Z&fromPlace=${startDetails.id}&toPlace=${endDetails.id}&arriveBy=false&timetableView=true&wheelchair=false&mode=TRANSIT,WALK`, - { method: 'GET' } - ); - - const data = await response.json(); - if (data) { - setTripDetails(data); // Set trip details if data is valid - } else { - setTripDetails(null); // Clear trip details if no valid trip data is found - } - - }; - - // Handle clicks outside of suggestion boxes to close them - const handleClickOutside = (event: MouseEvent) => { - if (startSuggestionsRef.current && !startSuggestionsRef.current.contains(event.target as Node)) { - setStartSuggestions([]); - } - if (endSuggestionsRef.current && !endSuggestionsRef.current.contains(event.target as Node)) { - setEndSuggestions([]); - } - }; - - useEffect(() => { - // Add event listener for click events - document.addEventListener('mousedown', handleClickOutside); - return () => { - // Clean up the event listener on component unmount - document.removeEventListener('mousedown', handleClickOutside); - }; - }, []); - - return ( - - {/* Left Side: Trip Planner Form */} - - Trip Planner - -
- - {startSuggestions.length > 0 && ( -
- {startSuggestions.map((suggestion, index) => ( -
{ - setStartLocation(suggestion.name); - setStartDetails({ lat: suggestion.lat, lon: suggestion.lon, id: suggestion.id }); - setStartSuggestions([]); // Clear suggestions on selection - }} - > - {suggestion.name} -
- ))} -
- )} -
- -
- - {endSuggestions.length > 0 && ( -
- {endSuggestions.map((suggestion, index) => ( -
{ - setEndLocation(suggestion.name); - setEndDetails({ lat: suggestion.lat, lon: suggestion.lon, id: suggestion.id }); - setEndSuggestions([]); // Clear suggestions on selection - }} - > - {suggestion.name} -
- ))} -
- )} -
- - -
-
- - {/* Right Side: Trip Details */} - - Trip Details - {tripDetails && ( - - {/* testing if working */} - {
{JSON.stringify(tripDetails, null, 2)}
} -
- )} -
-
- ); -}; - -export default TripPlannerOld; diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 7408fef..f4312ba 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -15,10 +15,10 @@ const HomePage: React.FC = () => { areas={[ 'header header', 'subheader subheader', - 'sidebar content', + 'content content', 'footer footer' ]} - columns={['1fr', '2fr']} + columns={['1fr', '1fr']} rows={['size-1000', "size-1000", '1fr', 'size-1000']} height="100vh" gap="size-0" @@ -29,12 +29,8 @@ const HomePage: React.FC = () => { - - {/* Sidebar can contain additional content if needed */} - {selectedView === 'planner' && } - - {/* Other content can be placed here */} + {selectedView === 'planner' && }