Repository: Soen390-Pathfinders/SOEN390_PathFinders
Pull request #346: Frontend sprint6 outdoor poi direction
Author: login: DominiqueProulx
Created: 2025-03-27T09:18:53Z | Merged: 2025-03-29T03:09:25Z

## 🚀 Summary
- Added Duration and Styling to the outdoor POI Info Box
- Collapsible hours in InfoBox
- Clicking on InfoBox Direciton button shows a path to the POI on the map
    - Reset destination button appear to remove the path
    - Estimated duration appear on the map 
- modified the radius for the slider so it aligns with the acceptance criterias   
Possible issue : flickering of the path -- I'll create a bug if it's not just me.

## 📌 Related Issue
#66  (#290 #291)
#64 (#289)

## ✅ Checklist

### **General Checks**
- [x] Code follows project conventions and best practices.
- [x] No unnecessary console logs or debugging statements.
- [x] Variables and functions are well-named and meaningful.
- [x] Code is modular and avoids repetition (DRY principle).

### **Functionality**
- [x] Does the feature work as expected?
- [x] Edge cases are handled properly.
- [ ] Bug fix addresses the issue completely (with verification).

### **Performance & Security**
- [x] No redundant computations.
- [x] No security vulnerabilities (e.g., unsafe input handling).

### **Testing & CI/CD**
- [ ] Unit tests exist for new functionality.
- [x] All tests pass in CI/CD pipeline.
- [ ] Code coverage is maintained or improved.

### **Documentation & Comments**
- [ ] Necessary comments are included for complex logic.
- [x] README/API documentation is updated if needed.

### 💬 Reviewer Notes
<!-- Leave comments for the reviewer or mention any areas you'd like extra attention on. -->

---
## Code Review
### **Reviewer Checklist** (To be filled by the reviewer)
- [ ] Code is clean and readable.
- [ ] Logic is correct and efficient.
- [ ] No major security concerns.
- [ ] No broken functionality or regressions.
- [ ] Test coverage is adequate and accurate.

### 💬 Reviewer Notes
<!-- Leave detailed comments for the author about the code, how it can be improved, or any best practices that can be applied. -->

### **Improvement Suggestions**
<!-- 
Provide any suggestions for improving the code quality, performance, readability, or structure.
If any areas could be refactored, recommend a cleaner or more efficient approach.
Any further optimization ideas or best practices that could be followed.
Is there a better way to handle edge cases or error handling? -->



Diff:
diff --git a/frontend/app/components/ui/OutdoorPOI_info.tsx b/frontend/app/components/ui/OutdoorPOI_info.tsx
index e0ffd2c..492ad5f 100644
--- a/frontend/app/components/ui/OutdoorPOI_info.tsx
+++ b/frontend/app/components/ui/OutdoorPOI_info.tsx
@@ -1,7 +1,23 @@
-import { View, Text, StyleSheet, Alert } from "react-native";
-import React from "react";
+import {
+  View,
+  Text,
+  StyleSheet,
+  Alert,
+  TouchableOpacity,
+  LayoutChangeEvent,
+} from "react-native";
+import { useState } from "react";
+import Animated, {
+  useSharedValue,
+  useAnimatedStyle,
+  withTiming,
+} from "react-native-reanimated";
+import Icon from "react-native-vector-icons/MaterialIcons";
+import useUserLocation from "@/app/hooks/useUserLocation";
+import getDistance from "geolib/es/getPreciseDistance";
+import { useLocation } from "../context/userLocationContext";
 
-export default function OutdoorPOI_info({ info }) {
+export default function OutdoorPOI_info({ info, onDirectionPress, placeId }) {
   //Deconstrcut the information from google Place API
   const {
     name,
@@ -21,36 +37,200 @@ export default function OutdoorPOI_info({ info }) {
   const { open_now, periods, weekday_text } = opening_hours || {};
   const { location, viewport } = geometry || {};
   const { lat, lng } = location || {};
+  const { userLocation, setLocation } = useUserLocation();
+  const {
+    userLocation: { latitude, longitude },
+  } = useUserLocation();
+
+  //For the height of the collapsioble
+  const [height, setHeight] = useState(0);
+  const animatedHeight = useSharedValue(0);
+  const [expanded, setExpanded] = useState(false);
+  const onLayout = (event: LayoutChangeEvent) => {
+    const onLayoutHeight = event.nativeEvent.layout.height;
+
+    if (onLayoutHeight > 0 && height !== onLayoutHeight) {
+      setHeight(onLayoutHeight);
+    }
+  };
+
+  const collapsableStyle = useAnimatedStyle(() => {
+    animatedHeight.value = expanded ? withTiming(height) : withTiming(0);
+
+    return {
+      height: animatedHeight.value,
+    };
+  }, [expanded, height]);
 
   return (
     <View style={styles.infoBoxContainer}>
-      <Text>Name : {name}</Text>
-      <Text>Distance:</Text>
-      <Text>userRatings: {rating} </Text>
-      <Text>Open Now:{open_now ? "Yes" : "No"} </Text>
-      {/* Add alert when place is currently closed */}
+      <Text style={styles.infoTitle}>{name}</Text>
+
+      <View style={styles.infoRow}>
+        <Text style={styles.infoLabel}>Distance:</Text>
+        <Text style={styles.infoValue}>
+          {location && userLocation
+            ? getDistance(
+                { latitude: location.lat, longitude: location.lng },
+                { latitude, longitude }
+              ) + " m"
+            : "Calculating..."}
+        </Text>
+      </View>
+
+      <View style={styles.infoRow}>
+        <Text style={styles.infoLabel}>Rating:</Text>
+        <Text style={styles.infoValue}>
+          {rating ? rating + " ★" : "No ratings"}
+        </Text>
+      </View>
+
+      <View style={styles.infoRow}>
+        <Text style={styles.infoLabel}>Status:</Text>
+        <Text style={styles.infoValue}>{open_now ? "Open now" : "Closed"}</Text>
+      </View>
+
+      {/* Alert when place is currently closed */}
       {open_now === false || open_now === "no" ? (
-        <Text style={{ color: "red", fontWeight: "bold", marginTop: 10 }}>
+        <Text style={styles.warningText}>
           This point of interest is currently closed
         </Text>
       ) : null}
-      <Text>Open Hours: </Text>
-      <Text>Hours:</Text>
-      {weekday_text && weekday_text.length > 0 ? (
-        weekday_text.map((day, index) => <Text key={index}>{day}</Text>)
-      ) : (
-        <Text>Operating hours not available</Text>
-      )}
+
+      <View style={styles.infoButton_container}>
+        <TouchableOpacity
+          style={styles.button}
+          onPress={() => {
+            onDirectionPress(placeId);
+          }}
+        >
+          <Icon name="directions" size={24} color="#FFFFFF" />
+          <Text style={styles.buttonText}>Directions</Text>
+        </TouchableOpacity>
+
+        <TouchableOpacity
+          style={styles.button}
+          onPress={() => setExpanded(!expanded)}
+        >
+          <Icon name="schedule" size={24} color="#FFFFFF" />
+          <Text style={styles.buttonText}>Schedule</Text>
+        </TouchableOpacity>
+      </View>
+
+      {/*Collapsible hours */}
+      <Animated.View style={[collapsableStyle, { overflow: "hidden" }]}>
+        <View
+          style={{ position: "absolute", width: "100%" }}
+          onLayout={onLayout}
+        >
+          <View style={styles.hoursContainer}>
+            <Text style={styles.hoursTitle}>Open Hours:</Text>
+            {weekday_text && weekday_text.length > 0 ? (
+              weekday_text.map((day, index) => (
+                <Text key={index} style={styles.hoursText}>
+                  {day}
+                </Text>
+              ))
+            ) : (
+              <Text style={styles.hoursText}>
+                Operating hours not available
+              </Text>
+            )}
+          </View>
+        </View>
+      </Animated.View>
     </View>
   );
 }
 const styles = StyleSheet.create({
   infoBoxContainer: {
-    marginTop: 20,
-    width: 300,
-    height: 400,
+    marginLeft: 70,
+    marginVertical: 10,
     backgroundColor: "white",
-    borderRadius: 10,
-    padding: 15,
+    borderRadius: 12,
+    padding: 12,
+    shadowColor: "#000",
+    shadowOffset: { width: 0, height: 1 },
+    shadowOpacity: 0.08,
+    shadowRadius: 2,
+    elevation: 2,
+    borderWidth: 1,
+    borderColor: "#f0f0f0",
+  },
+  infoTitle: {
+    fontSize: 16,
+    fontWeight: "700",
+    marginBottom: 6,
+    color: "#333333",
+  },
+  infoRow: {
+    flexDirection: "row",
+    alignItems: "center",
+    marginBottom: 4,
+  },
+  infoLabel: {
+    fontWeight: "600",
+    marginRight: 5,
+    color: "#555555",
+    fontSize: 13,
+  },
+  infoValue: {
+    color: "#333333",
+    fontSize: 13,
+  },
+  warningText: {
+    color: "#D8000C",
+    fontWeight: "600",
+    marginTop: 6,
+    backgroundColor: "#FFBABA",
+    padding: 5,
+    borderRadius: 6,
+    textAlign: "center",
+    fontSize: 12,
+  },
+  infoButton_container: {
+    marginTop: 8,
+    marginBottom: 8,
+    flexDirection: "row",
+    justifyContent: "center",
+    alignItems: "center",
+    gap: 10,
+  },
+  button: {
+    backgroundColor: "#912338",
+    flexDirection: "row",
+    alignItems: "center",
+    justifyContent: "center",
+    paddingVertical: 6,
+    paddingHorizontal: 12,
+    borderRadius: 8,
+    gap: 6,
+    elevation: 1,
+    shadowColor: "#000",
+    shadowOffset: { width: 0, height: 1 },
+    shadowOpacity: 0.1,
+    shadowRadius: 1,
+  },
+  buttonText: {
+    color: "#FFFFFF",
+    fontSize: 14,
+    fontWeight: "600",
+  },
+  hoursContainer: {
+    backgroundColor: "#f7f7f7",
+    borderRadius: 6,
+    padding: 8,
+    marginTop: 3,
+  },
+  hoursTitle: {
+    fontWeight: "600",
+    marginBottom: 3,
+    color: "#555555",
+    fontSize: 13,
+  },
+  hoursText: {
+    marginBottom: 2,
+    color: "#555555",
+    fontSize: 12,
   },
 });
diff --git a/frontend/app/components/ui/RadiusButton.tsx b/frontend/app/components/ui/RadiusButton.tsx
index c58cd11..4d8f8ec 100644
--- a/frontend/app/components/ui/RadiusButton.tsx
+++ b/frontend/app/components/ui/RadiusButton.tsx
@@ -6,10 +6,8 @@ interface RadiusSliderProps {
   onRadiusChange: (radius: number) => void; // Called while sliding
 }
 
-const RadiusSlider: React.FC<RadiusSliderProps> = ({
-  onRadiusChange,
-}) => {
-  const [radius, setRadius] = useState(1000); // Default to 1km
+const RadiusSlider: React.FC<RadiusSliderProps> = ({ onRadiusChange }) => {
+  const [radius, setRadius] = useState(500); //Default changed to 500 to adhere to acceptance criteria
 
   const handleValueChange = (value: number) => {
     setRadius(value);
@@ -18,7 +16,7 @@ const RadiusSlider: React.FC<RadiusSliderProps> = ({
 
   return (
     <View style={styles.container}>
-    <Text style={styles.text}>{`Search radius: ${Math.round(radius)}m`}</Text>
+      <Text style={styles.text}>{`Search radius: ${Math.round(radius)}m`}</Text>
       <Slider
         style={styles.slider}
         minimumValue={200}
@@ -35,16 +33,16 @@ const RadiusSlider: React.FC<RadiusSliderProps> = ({
 };
 
 const styles = StyleSheet.create({
-    container: {
-      padding: 20,
-      marginVertical: 10,
-      backgroundColor: '#F8F8F8',
-      borderRadius: 8,
-    },
-    slider: {
-        width: "100%",
-        height: 10,
-      },
+  container: {
+    padding: 20,
+    marginVertical: 10,
+    backgroundColor: "#F8F8F8",
+    borderRadius: 8,
+  },
+  slider: {
+    width: "100%",
+    height: 10,
+  },
   text: {
     fontSize: 16,
     marginBottom: 10,
@@ -52,4 +50,4 @@ const styles = StyleSheet.create({
   },
 });
 
-export default RadiusSlider;
\ No newline at end of file
+export default RadiusSlider;
diff --git a/frontend/app/hooks/useUserLocation.ts b/frontend/app/hooks/useUserLocation.ts
index c8cf58d..7079b4a 100644
--- a/frontend/app/hooks/useUserLocation.ts
+++ b/frontend/app/hooks/useUserLocation.ts
@@ -38,7 +38,7 @@ const useUserLocation = () => {
     // Set up interval to fetch location every 5 seconds
     const intervalId = setInterval(() => {
       getUserLocation();
-    }, 2000); // update the user location every 2 seconds
+    }, 3000); // update the user location every 3 seconds
 
 
     return () => {
diff --git a/frontend/app/screens/OutdoorPointsOfInterests.tsx b/frontend/app/screens/OutdoorPointsOfInterests.tsx
index b5a9ec5..fb8bbde 100644
--- a/frontend/app/screens/OutdoorPointsOfInterests.tsx
+++ b/frontend/app/screens/OutdoorPointsOfInterests.tsx
@@ -1,5 +1,5 @@
 import React from "react";
-import { useState, useEffect } from "react";
+import { useState, useEffect, useMemo } from "react";
 import { View, Text, StyleSheet, Button, TouchableOpacity } from "react-native";
 import useTheme from "../hooks/useTheme";
 import { getStyles } from "../styles";
@@ -7,6 +7,7 @@ import CampusPilotHeader from "../components/ui/CampusPilotHeader";
 import { CampusToggle } from "../components/ui/CampusToggle";
 import OutdoorPOI_info from "../components/ui/OutdoorPOI_info";
 import useFetchGooglePlacesInfo from "../hooks/useFetchGooglePlaceInfo";
+import { GOOGLE_MAPS_APIKEY } from "../constants";
 import { Ionicons } from "@expo/vector-icons";
 import FilterPOI from "../components/ui/FilterPOI";
 import MapView, {
@@ -14,14 +15,20 @@ import MapView, {
   PROVIDER_DEFAULT,
   PROVIDER_GOOGLE,
 } from "react-native-maps";
+import MapViewDirections from "react-native-maps-directions";
 import RadiusSlider from "../components/ui/RadiusButton";
+import useUserLocation from "../hooks/useUserLocation";
 
 export default function OutdoorPointsOfInterests() {
+  const mapRef = React.useRef(null);
   const { theme } = useTheme();
   const globalStyles = getStyles(theme);
   const [campus, setCampus] = useState("SGW"); // Default to SGW
   const [activeFilter, setActiveFilter] = useState(null);
   const [searchRadius, setSearchRadius] = useState(1000); // Default to 1km
+  const { userLocation, setLocation } = useUserLocation(); // to set the poi around the puser
+  const [duration, setDuration] = useState(null);
+  const [destination, setDestination] = useState(null);
 
   const toggleCampus = (selectedCampus) => {
     setCampus(selectedCampus);
@@ -58,10 +65,7 @@ export default function OutdoorPointsOfInterests() {
     fetchPlaceInfo,
     filteredPlaces,
     fetchPlacesByCategories,
-  } = useFetchGooglePlacesInfo({
-    placeID: outdoorPlaceID,
-    searchRadius,
-  });
+  } = useFetchGooglePlacesInfo({ placeID: outdoorPlaceID, searchRadius });
   // Show component when place changes
   useEffect(() => {
     if (placeInfo) {
@@ -76,20 +80,30 @@ export default function OutdoorPointsOfInterests() {
   const handleFilterPress = (filters) => {
     setActiveFilter(filters);
     // Call the new function from our hook to fetch places by category
-    fetchPlacesByCategories(filters, campusCoordinates[campus]);
+    fetchPlacesByCategories(filters, userLocation); // set to user location
+    // fetchPlacesByCategories(filters, campusCoordinates[campus]);
   };
   const handleMarkerPress = (placeId) => {
     setoutdoorPlaceID(placeId);
-    fetchPlaceInfo(placeId);
+    //API call will be made automatically from the hook
   };
 
   const handleRadiusChange = (radius: number) => {
     setSearchRadius(radius);
     if (activeFilter) {
-      fetchPlacesByCategories(activeFilter, campusCoordinates[campus]);
+      //  fetchPlacesByCategories(activeFilter, campusCoordinates[campus]);
+      fetchPlacesByCategories(activeFilter, userLocation);
     }
   };
 
+  const handleDirectionPress = (place) => {
+    closeInfoBox();
+    setDestination(place);
+  };
+  const resetDestination = () => {
+    setDestination(null);
+  };
+
   return (
     <View style={globalStyles.container}>
       <CampusPilotHeader />
@@ -103,7 +117,11 @@ export default function OutdoorPointsOfInterests() {
                 <Ionicons name="close" size={24} color="white" />
               </TouchableOpacity>
             </View>
-            <OutdoorPOI_info info={placeInfo} />
+            <OutdoorPOI_info
+              info={placeInfo}
+              onDirectionPress={handleDirectionPress}
+              placeId={place}
+            />
           </View>
         )}
         <RadiusSlider
@@ -111,6 +129,7 @@ export default function OutdoorPointsOfInterests() {
         />
 
         <MapView
+          ref={mapRef}
           showsUserLocation={true}
           provider={PROVIDER_DEFAULT}
           style={{ flex: 1 }}
@@ -129,8 +148,43 @@ export default function OutdoorPointsOfInterests() {
               onPress={() => handleMarkerPress(place.place_id)}
             />
           ))}
+          {/*Show direciton if the destionation and user location are set*/}
+          {userLocation && destination && (
+            <MapViewDirections
+              origin={userLocation}
+              destination={`place_id:${destination}`}
+              apikey={GOOGLE_MAPS_APIKEY}
+              mode={"WALKING"}
+              strokeWidth={7}
+              strokeColor="blue"
+              onReady={(result) => {
+                setDuration(result.duration);
+              }}
+              onError={(errorMessage) => {
+                console.log("Directions error: ", errorMessage);
+              }}
+            />
+          )}
         </MapView>
-        <FilterPOI onFilterPress={handleFilterPress} />
+        <View style={styles.buttonsOverMap}>
+          <FilterPOI onFilterPress={handleFilterPress} />
+          {destination && (
+            <View style={styles.resetDestinationButton}>
+              <TouchableOpacity onPress={resetDestination}>
+                <Text style={{ color: "#FFFFFF" }}>Reset Destination</Text>
+              </TouchableOpacity>
+            </View>
+          )}
+          {/*//TODO: refactor this into one view and one conditional statement
+          // The Buttons for reset Destionation and time do not show if there is no path*/}
+          {destination && (
+            <View style={styles.durationText}>
+              <Text style={{ color: "#ffffff" }}>
+                {Math.round(duration)} min
+              </Text>
+            </View>
+          )}
+        </View>
       </View>
     </View>
   );
@@ -151,11 +205,40 @@ const styles = StyleSheet.create({
   infoBoxCloseButton: {
     position: "absolute", // Make sure the box is on top of the map
     top: 15,
-    left: 250,
+    right: 10,
     zIndex: 2,
     margin: 10,
     padding: 5,
     backgroundColor: "rgba(145, 35, 55, 0.99)",
     borderRadius: 50,
   },
+  buttonsOverMap: {
+    position: "absolute", // Make sure the button is on top of the map
+    bottom: 20,
+    left: 20,
+    zIndex: 2,
+    flexDirection: "row",
+    justifyContent: "space-between",
+    alignItems: "center",
+    margin: 10,
+    padding: 5,
+    borderRadius: 50,
+  },
+  durationText: {
+    //position: "absolute",
+    zIndex: 4,
+    // top: 100,
+    // left: 10,
+    backgroundColor: "#0072a8",
+    padding: 10,
+    margin: 10,
+    color: "white",
+    borderRadius: 10,
+  },
+  resetDestinationButton: {
+    backgroundColor: "#0072a8",
+    padding: 10,
+    color: "white",
+    borderRadius: 10,
+  },
 });


Reviews:
user: login: HawaSaid
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T16:34:24Z
user: login: HawaSaid
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T16:34:57Z
user: login: HawaSaid
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T16:40:18Z
user: login: HawaSaid
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T16:40:57Z
user: login: HawaSaid
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T16:42:21Z
user: login: HawaSaid
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T16:42:55Z
user: login: HawaSaid
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-28T16:43:19Z

Review comments:
diff_hunk: @@ -6,10 +6,8 @@ interface RadiusSliderProps {
   onRadiusChange: (radius: number) => void; // Called while sliding
 }
 
-const RadiusSlider: React.FC<RadiusSliderProps> = ({
-  onRadiusChange,
-}) => {
-  const [radius, setRadius] = useState(1000); // Default to 1km
+const RadiusSlider: React.FC<RadiusSliderProps> = ({ onRadiusChange }) => {
+  const [radius, setRadius] = useState(500); //Default changed to 500 to adhere to acceptance criteria
path: frontend/app/components/ui/RadiusButton.tsx
user: login: HawaSaid
body: Thank you for changing this to match the requirements!
created_at: 2025-03-28T16:34:24Z
author_association: COLLABORATOR
diff_hunk: @@ -38,7 +38,7 @@ const useUserLocation = () => {
     // Set up interval to fetch location every 5 seconds
     const intervalId = setInterval(() => {
       getUserLocation();
-    }, 2000); // update the user location every 2 seconds
+    }, 3000); // update the user location every 3 seconds
path: frontend/app/hooks/useUserLocation.ts
user: login: HawaSaid
body: Good idea to update the user's location every 3 second instead of 2!
created_at: 2025-03-28T16:34:57Z
author_association: COLLABORATOR
diff_hunk: @@ -35,21 +33,21 @@ const RadiusSlider: React.FC<RadiusSliderProps> = ({
 };
 
 const styles = StyleSheet.create({
-    container: {
-      padding: 20,
-      marginVertical: 10,
-      backgroundColor: '#F8F8F8',
-      borderRadius: 8,
-    },
-    slider: {
-        width: "100%",
-        height: 10,
-      },
+  container: {
path: frontend/app/components/ui/RadiusButton.tsx
user: login: HawaSaid
body: Maintaining consistent code formatting across the styles object helps improve readability. Looks good!


created_at: 2025-03-28T16:40:18Z
author_association: COLLABORATOR
diff_hunk: @@ -1,7 +1,23 @@
-import { View, Text, StyleSheet, Alert } from "react-native";
-import React from "react";
+import {
+  View,
+  Text,
+  StyleSheet,
+  Alert,
+  TouchableOpacity,
+  LayoutChangeEvent,
+} from "react-native";
+import { useState } from "react";
+import Animated, {
+  useSharedValue,
+  useAnimatedStyle,
+  withTiming,
+} from "react-native-reanimated";
+import Icon from "react-native-vector-icons/MaterialIcons";
+import useUserLocation from "@/app/hooks/useUserLocation";
+import getDistance from "geolib/es/getPreciseDistance";
+import { useLocation } from "../context/userLocationContext";
 
-export default function OutdoorPOI_info({ info }) {
+export default function OutdoorPOI_info({ info, onDirectionPress, placeId }) {
path: frontend/app/components/ui/OutdoorPOI_info.tsx
user: login: HawaSaid
body: Great addition of onDirectionPress and placeId as props! This improves reusability and makes the component more flexible!
created_at: 2025-03-28T16:40:57Z
author_association: COLLABORATOR
diff_hunk: @@ -21,36 +37,200 @@ export default function OutdoorPOI_info({ info }) {
   const { open_now, periods, weekday_text } = opening_hours || {};
   const { location, viewport } = geometry || {};
   const { lat, lng } = location || {};
+  const { userLocation, setLocation } = useUserLocation();
+  const {
+    userLocation: { latitude, longitude },
+  } = useUserLocation();
+
+  //For the height of the collapsioble
+  const [height, setHeight] = useState(0);
+  const animatedHeight = useSharedValue(0);
+  const [expanded, setExpanded] = useState(false);
+  const onLayout = (event: LayoutChangeEvent) => {
+    const onLayoutHeight = event.nativeEvent.layout.height;
+
+    if (onLayoutHeight > 0 && height !== onLayoutHeight) {
+      setHeight(onLayoutHeight);
+    }
+  };
+
+  const collapsableStyle = useAnimatedStyle(() => {
+    animatedHeight.value = expanded ? withTiming(height) : withTiming(0);
+
+    return {
+      height: animatedHeight.value,
+    };
+  }, [expanded, height]);
 
   return (
     <View style={styles.infoBoxContainer}>
-      <Text>Name : {name}</Text>
-      <Text>Distance:</Text>
-      <Text>userRatings: {rating} </Text>
-      <Text>Open Now:{open_now ? "Yes" : "No"} </Text>
-      {/* Add alert when place is currently closed */}
+      <Text style={styles.infoTitle}>{name}</Text>
path: frontend/app/components/ui/OutdoorPOI_info.tsx
user: login: HawaSaid
body: I like the structured way you display ratings and status now! The improved layout enhances readability.
created_at: 2025-03-28T16:42:21Z
author_association: COLLABORATOR
diff_hunk: @@ -21,36 +37,200 @@ export default function OutdoorPOI_info({ info }) {
   const { open_now, periods, weekday_text } = opening_hours || {};
   const { location, viewport } = geometry || {};
   const { lat, lng } = location || {};
+  const { userLocation, setLocation } = useUserLocation();
+  const {
+    userLocation: { latitude, longitude },
+  } = useUserLocation();
+
+  //For the height of the collapsioble
+  const [height, setHeight] = useState(0);
+  const animatedHeight = useSharedValue(0);
+  const [expanded, setExpanded] = useState(false);
+  const onLayout = (event: LayoutChangeEvent) => {
+    const onLayoutHeight = event.nativeEvent.layout.height;
+
+    if (onLayoutHeight > 0 && height !== onLayoutHeight) {
+      setHeight(onLayoutHeight);
+    }
+  };
+
+  const collapsableStyle = useAnimatedStyle(() => {
+    animatedHeight.value = expanded ? withTiming(height) : withTiming(0);
+
+    return {
+      height: animatedHeight.value,
+    };
+  }, [expanded, height]);
 
   return (
     <View style={styles.infoBoxContainer}>
-      <Text>Name : {name}</Text>
-      <Text>Distance:</Text>
-      <Text>userRatings: {rating} </Text>
-      <Text>Open Now:{open_now ? "Yes" : "No"} </Text>
-      {/* Add alert when place is currently closed */}
+      <Text style={styles.infoTitle}>{name}</Text>
+
+      <View style={styles.infoRow}>
+        <Text style={styles.infoLabel}>Distance:</Text>
+        <Text style={styles.infoValue}>
+          {location && userLocation
+            ? getDistance(
path: frontend/app/components/ui/OutdoorPOI_info.tsx
user: login: HawaSaid
body: Good catch on improving the `getDistance` logic! The fallback text ‘Calculating…’ provides a better user experience when location data is unavailable.
created_at: 2025-03-28T16:42:55Z
author_association: COLLABORATOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-27T09:21:52Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=Soen390-Pathfinders_SOEN390_PathFinders&pullRequest=346) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [10 New issues](https://sonarcloud.io/project/issues?id=Soen390-Pathfinders_SOEN390_PathFinders&pullRequest=346&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=Soen390-Pathfinders_SOEN390_PathFinders&pullRequest=346&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=Soen390-Pathfinders_SOEN390_PathFinders&pullRequest=346&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=Soen390-Pathfinders_SOEN390_PathFinders&pullRequest=346&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=Soen390-Pathfinders_SOEN390_PathFinders&pullRequest=346&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=Soen390-Pathfinders_SOEN390_PathFinders&pullRequest=346)


author_association: NONE