From 896e9ecd435086b21ee66db9fcd4fcfc5e0ee5be Mon Sep 17 00:00:00 2001
From: "Jihun (James) Doh" <dohj0109@sas.upenn.edu>
Date: Sat, 21 Dec 2024 19:22:59 -0500
Subject: [PATCH] dropdown to  radio for map view + comments added & minor bu g
 fixes

---
 frontend/plan/components/map/Map.tsx          |  41 ++---
 .../plan/components/map/MapCourseItem.tsx     |  58 ++++---
 frontend/plan/components/map/MapDropdown.tsx  | 143 ------------------
 frontend/plan/components/map/MapRadio.tsx     |  72 +++++++++
 frontend/plan/components/map/MapTab.tsx       |  27 ++--
 frontend/plan/components/meetUtil.tsx         |   4 +-
 frontend/plan/constants/constants.js          |   2 -
 frontend/plan/pages/index.tsx                 |   8 +-
 frontend/plan/types.ts                        |   7 +-
 9 files changed, 152 insertions(+), 210 deletions(-)
 delete mode 100644 frontend/plan/components/map/MapDropdown.tsx
 create mode 100644 frontend/plan/components/map/MapRadio.tsx

diff --git a/frontend/plan/components/map/Map.tsx b/frontend/plan/components/map/Map.tsx
index 9694ad68..66a91aaa 100644
--- a/frontend/plan/components/map/Map.tsx
+++ b/frontend/plan/components/map/Map.tsx
@@ -16,6 +16,7 @@ function toDegrees(radians: number): number {
     return radians * (180 / Math.PI);
 }
 
+// calculate the center of all locations on the map view to center the map 
 function getGeographicCenter(
     locations: Location[]
 ): [number, number] {
@@ -38,39 +39,44 @@ function getGeographicCenter(
     y /= total;
     z /= total;
 
-    const centralLongitude = Math.atan2(y, x);
+    const centralLongitude = toDegrees(Math.atan2(y, x));
     const centralSquareRoot = Math.sqrt(x * x + y * y);
-    const centralLatitude = Math.atan2(z, centralSquareRoot);
+    const centralLatitude = toDegrees(Math.atan2(z, centralSquareRoot));
 
-    return [toDegrees(centralLatitude), toDegrees(centralLongitude)];
+    return [centralLatitude ? centralLatitude : 39.9515, centralLongitude ? centralLongitude : -75.1910];
 }
 
 function separateOverlappingPoints(points: Location[], offset = 0.0001) {
     const validPoints = points.filter((p) => p.lat !== null && p.lng !== null) as Location[];
 
     // group points by coordinates
-    const groupedPoints: Record<string, Location[]> = validPoints.reduce((acc, point) => {
+    const groupedPoints: Record<string, Location[]> = {}; 
+    validPoints.forEach((point) => {
         const key = `${point.lat},${point.lng}`;
-        (acc[key] ||= []).push(point);
-        return acc;
-    }, {} as Record<string, Location[]>);
+        (groupedPoints[key] ||= []).push(point);
+    });
 
     // adjust overlapping points
     const adjustedPoints = Object.values(groupedPoints).flatMap((group) =>
         group.length === 1
-            ? group
+            ? group // no adjustment needed if class in map view doesnt share locations with others
             : group.map((point, index) => {
-                  const angle = (2 * Math.PI * index) / group.length;
-                  return {
-                      ...point,
-                      lat: point.lat! + offset * Math.cos(angle),
-                      lng: point.lng! + offset * Math.sin(angle),
-                  };
-              })
+                /*
+                At a high level, if there are multiple classes in map view that have the exact same location, 
+                we try to evenly distribute them around a "circle" centered on the original location. 
+                The size of the circle is determined by offset. 
+                 */
+                const angle = (2 * Math.PI * index) / group.length;
+                return {
+                    ...point,
+                    lat: point.lat! + offset * Math.cos(angle),
+                    lng: point.lng! + offset * Math.sin(angle),
+                };
+            })
     );
 
     // include points with null values
-    return [...adjustedPoints, ...points.filter((p) => p.lat === null || p.lng === null)];
+    return adjustedPoints;
 }
 
 interface InnerMapProps {
@@ -78,6 +84,7 @@ interface InnerMapProps {
     center: [number, number]
 }
 
+// need inner child component to use useMap hook to run on client 
 function InnerMap({ locations, center } :InnerMapProps) {
     const map = useMap();
 
@@ -101,7 +108,7 @@ function InnerMap({ locations, center } :InnerMapProps) {
 }
 
 function Map({ locations, zoom }: MapProps) {
-    const center = getGeographicCenter(locations)
+    const center = getGeographicCenter(locations);
     
     return (
         <MapContainer
diff --git a/frontend/plan/components/map/MapCourseItem.tsx b/frontend/plan/components/map/MapCourseItem.tsx
index 46adfaa3..f43702db 100644
--- a/frontend/plan/components/map/MapCourseItem.tsx
+++ b/frontend/plan/components/map/MapCourseItem.tsx
@@ -1,7 +1,10 @@
 import styled from "styled-components";
 import { isMobile } from "react-device-detect";
 
-const CourseCartItem = styled.div<{ $isMobile: boolean }>`
+const CourseCartItem = styled.div<{
+    $isMobile: boolean;
+    $hasLocationData: boolean;
+}>`
     background: white;
     transition: 250ms ease background;
     cursor: pointer;
@@ -24,10 +27,13 @@ const CourseCartItem = styled.div<{ $isMobile: boolean }>`
     &:hover i {
         color: #d3d3d8;
     }
+    ${({ $hasLocationData }) =>
+        !$hasLocationData &&
+        `
+        opacity: 0.5;
+    `}
 `;
 
-const CourseDetailsContainer = styled.div``;
-
 const Dot = styled.span<{ $color: string }>`
     height: 5px;
     width: 5px;
@@ -42,7 +48,7 @@ interface CourseDetailsProps {
     start: number;
     end: number;
     room: string;
-    overlap: boolean;
+    hasLocationData: boolean;
 }
 
 const getTimeString = (start: number, end: number) => {
@@ -67,35 +73,24 @@ const CourseDetails = ({
     start,
     end,
     room,
-    overlap,
+    hasLocationData,
 }: CourseDetailsProps) => (
-    <CourseDetailsContainer>
+    <div>
         <b>
             <span>{id.replace(/-/g, " ")}</span>
         </b>
-        <div style={{ fontSize: "0.8rem" }}>
-            <div style={{ display: "flex", justifyContent: "space-between" }}>
-                <div>
-                    {overlap && (
-                        <div className="popover is-popover-right">
-                            <i
-                                style={{
-                                    paddingRight: "5px",
-                                    color: "#c6c6c6",
-                                }}
-                                className="fas fa-calendar-times"
-                            />
-                            <span className="popover-content">
-                                Conflicts with schedule!
-                            </span>
-                        </div>
-                    )}
-                    {getTimeString(start, end)}
-                </div>
-                <div>{room ? room : "No room data"}</div>
-            </div>
+
+        <div
+            style={{
+                display: "flex",
+                fontSize: "0.8rem",
+            }}
+        >
+            <div>{getTimeString(start, end)}</div>
+            &nbsp;&nbsp;
+            <div>{room && hasLocationData ? room : "No room data"}</div>
         </div>
-    </CourseDetailsContainer>
+    </div>
 );
 
 interface CartSectionProps {
@@ -104,7 +99,7 @@ interface CartSectionProps {
     start: number;
     end: number;
     room: string;
-    overlap: boolean;
+    hasLocationData: boolean;
     focusSection: (id: string) => void;
 }
 
@@ -114,7 +109,7 @@ function MapCourseItem({
     start,
     end,
     room,
-    overlap,
+    hasLocationData,
     focusSection,
 }: CartSectionProps) {
     return (
@@ -123,6 +118,7 @@ function MapCourseItem({
             id={id}
             aria-checked="false"
             $isMobile={isMobile}
+            $hasLocationData={hasLocationData}
             onClick={() => {
                 const split = id.split("-");
                 focusSection(`${split[0]}-${split[1]}`);
@@ -134,7 +130,7 @@ function MapCourseItem({
                 start={start}
                 end={end}
                 room={room}
-                overlap={overlap}
+                hasLocationData={hasLocationData}
             />
         </CourseCartItem>
     );
diff --git a/frontend/plan/components/map/MapDropdown.tsx b/frontend/plan/components/map/MapDropdown.tsx
deleted file mode 100644
index 13602314..00000000
--- a/frontend/plan/components/map/MapDropdown.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
-import styled from "styled-components";
-import { Icon } from "../bulma_derived_components";
-import { DAYS_TO_DAYSTRINGS } from "../../constants/constants";
-import { Day } from "../../types";
-
-const DropdownContainer = styled.div`
-    margin-top: 5px;
-    border-radius: 0.5rem;
-    border: 0;
-    outline: none;
-    display: inline-flex;
-    vertical-align: top;
-    width: 100%;
-    position: relative;
-    * {
-        border: 0;
-        outline: none;
-    }
-`;
-
-const DropdownTriggerContainer = styled.div<{ $isActive: boolean }>`
-    padding-left: 0.5rem;
-    display: flex;
-
-    align-items: center;
-    background: ${({ $isActive }: { $isActive: boolean }) =>
-        $isActive ? "rgba(162, 180, 237, 0.38) !important" : "none"};
-
-    :hover {
-        background: rgba(175, 194, 255, 0.27);
-    }
-`;
-
-const DropdownTrigger = styled.div`
-    margin-left: 1rem;
-    height: 1.5rem;
-
-    text-align: center;
-    outline: none !important;
-    border: none !important;
-    background: transparent;
-`;
-
-const DropdownText = styled.div`
-    font-size: 1vw;
-    font-weight: 600;
-`;
-
-const DropdownMenu = styled.div<{ $isActive: boolean }>`
-    margin-top: 0.1rem !important;
-    display: ${({ $isActive }) => ($isActive ? "block" : "none")};
-    left: 0;
-    min-width: 9rem;
-    padding-top: 4px;
-    position: absolute;
-    top: 100%;
-    z-index: 2000 !important;
-`;
-
-const DropdownContent = styled.div`
-    background-color: #fff;
-    box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1);
-    padding: 0;
-`;
-
-const DropdownButton = styled.div`
-    padding-left: 6px;
-    &:hover {
-        background: #f5f6f8;
-    }
-`;
-
-interface MapDropdownProps {
-    selectedDay: Day;
-    setSelectedDay: Dispatch<SetStateAction<Day>>;
-}
-export default function MapDropdown({
-    selectedDay,
-    setSelectedDay,
-}: MapDropdownProps) {
-    const [isActive, setIsActive] = useState(false);
-
-    const ref = useRef<HTMLDivElement>(null);
-
-    useEffect(() => {
-        const listener = (event: Event) => {
-            if (
-                ref.current &&
-                !ref.current.contains(event.target as HTMLElement)
-            ) {
-                setIsActive(false);
-            }
-        };
-        document.addEventListener("click", listener);
-        return () => {
-            document.removeEventListener("click", listener);
-        };
-    });
-
-    return (
-        <DropdownContainer ref={ref}>
-            <DropdownTriggerContainer
-                $isActive={isActive}
-                onClick={() => {
-                    setIsActive(!isActive);
-                }}
-                role="button"
-            >
-                <DropdownText>{DAYS_TO_DAYSTRINGS[selectedDay]}</DropdownText>
-                <DropdownTrigger>
-                    <div aria-haspopup={true} aria-controls="dropdown-menu">
-                        <Icon>
-                            <i
-                                className={`fa fa-chevron-${
-                                    isActive ? "up" : "down"
-                                }`}
-                                aria-hidden="true"
-                            />
-                        </Icon>
-                    </div>
-                </DropdownTrigger>
-            </DropdownTriggerContainer>
-            <DropdownMenu $isActive={isActive} role="menu">
-                <DropdownContent>
-                    {Object.entries(DAYS_TO_DAYSTRINGS).map(
-                        ([day, dayString]) => (
-                            <DropdownButton
-                                key={day}
-                                onClick={() => {
-                                    setSelectedDay(day as Day);
-                                    setIsActive(false);
-                                }}
-                            >
-                                {dayString}
-                            </DropdownButton>
-                        )
-                    )}
-                </DropdownContent>
-            </DropdownMenu>
-        </DropdownContainer>
-    );
-}
diff --git a/frontend/plan/components/map/MapRadio.tsx b/frontend/plan/components/map/MapRadio.tsx
new file mode 100644
index 00000000..d0b3a1b4
--- /dev/null
+++ b/frontend/plan/components/map/MapRadio.tsx
@@ -0,0 +1,72 @@
+import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
+import styled from "styled-components";
+import { Icon } from "../bulma_derived_components";
+import { DAYS_TO_DAYSTRINGS } from "../../constants/constants";
+import { Day, Weekdays } from "../../types";
+
+const RadioContainer = styled.div`
+    display: flex;
+    justify-content: space-between;
+    padding: 9px 8px 5px 8px;
+    width: 100%;
+`;
+
+const Radio = styled.div<{ $isSelected: boolean }>`
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    border-radius: 50%;
+    width: 25px;
+    height: 25px;
+    cursor: pointer;
+    font-weight: 500;
+    font-size: 12px;
+    background: ${({ $isSelected }: { $isSelected: boolean }) =>
+        $isSelected ? "#868ED8 !important" : "#F4F4F4 !important"};
+    color: ${({ $isSelected }: { $isSelected: boolean }) =>
+        $isSelected && "white !important"};
+`;
+
+interface MapDropdownProps {
+    selectedDay: Day;
+    setSelectedDay: Dispatch<SetStateAction<Day>>;
+}
+export default function MapDropdown({
+    selectedDay,
+    setSelectedDay,
+}: MapDropdownProps) {
+    const [isActive, setIsActive] = useState(false);
+
+    const ref = useRef<HTMLDivElement>(null);
+
+    useEffect(() => {
+        const listener = (event: Event) => {
+            if (
+                ref.current &&
+                !ref.current.contains(event.target as HTMLElement)
+            ) {
+                setIsActive(false);
+            }
+        };
+        document.addEventListener("click", listener);
+        return () => {
+            document.removeEventListener("click", listener);
+        };
+    });
+
+    return (
+        <RadioContainer>
+            {Object.keys(DAYS_TO_DAYSTRINGS).map((day) => (
+                <Radio
+                    $isSelected={selectedDay === day}
+                    onClick={() => {
+                        setSelectedDay(day as Day);
+                        setIsActive(false);
+                    }}
+                >
+                    <span>{day}</span>
+                </Radio>
+            ))}
+        </RadioContainer>
+    );
+}
diff --git a/frontend/plan/components/map/MapTab.tsx b/frontend/plan/components/map/MapTab.tsx
index b95d5f17..aa93b057 100644
--- a/frontend/plan/components/map/MapTab.tsx
+++ b/frontend/plan/components/map/MapTab.tsx
@@ -2,11 +2,11 @@ import React, { useState } from "react";
 import { connect } from "react-redux";
 import dynamic from "next/dynamic";
 import styled from "styled-components";
-import MapDropdown from "./MapDropdown";
+import MapRadio from "./MapRadio";
 import MapCourseItem from "./MapCourseItem";
 import { scheduleContainsSection } from "../meetUtil";
 import { DAYS_TO_DAYSTRINGS } from "../../constants/constants";
-import { Section, Meeting, Day } from "../../types";
+import { Section, Meeting, Day, Weekdays } from "../../types";
 import "leaflet/dist/leaflet.css";
 import { ThunkDispatch } from "redux-thunk";
 import { fetchCourseDetails } from "../../actions";
@@ -49,9 +49,8 @@ const Box = styled.section<{ $length: number }>`
 
 const MapContainer = styled.div`
     height: 40%;
-    padding-left: 7px;
-    padding-right: 7px;
-    margin-right: 5px;
+    margin-right: 8px;
+    margin-left: 8px;
     margin-top: 5px;
 `;
 
@@ -87,7 +86,9 @@ const DayEmpty = ({ day }: DayEmptyProps) => (
                 marginBottom: "0.5rem",
             }}
         >
-            {`You don't have classes on ${DAYS_TO_DAYSTRINGS[day as Day]}!`}
+            {`You don't have classes on ${
+                DAYS_TO_DAYSTRINGS[day as Weekdays]
+            }!`}
         </h3>
         Click a course section&apos;s + icon to add it to the schedule.
         <br />
@@ -96,12 +97,12 @@ const DayEmpty = ({ day }: DayEmptyProps) => (
 );
 
 function MapTab({ meetingsByDay, focusSection }: MabTapProps) {
-    const [selectedDay, setSelectedDay] = useState<Day>(Day.M);
+    const [selectedDay, setSelectedDay] = useState<Day>(Weekdays.M);
     const meeetingsForDay = meetingsByDay[selectedDay];
 
     return (
         <Box $length={meeetingsForDay.length} id="cart">
-            <MapDropdown
+            <MapRadio
                 selectedDay={selectedDay}
                 setSelectedDay={setSelectedDay}
             />
@@ -122,7 +123,7 @@ function MapTab({ meetingsByDay, focusSection }: MabTapProps) {
                                         locData.lat != null &&
                                         locData.lng != null
                                 )}
-                            zoom={15}
+                            zoom={14}
                         />
                     </MapContainer>
                     <MapCourseItemcontainer>
@@ -131,10 +132,11 @@ function MapTab({ meetingsByDay, focusSection }: MabTapProps) {
                                 {
                                     id,
                                     color,
+                                    latitude,
+                                    longitude,
                                     start,
                                     end,
                                     room,
-                                    overlap,
                                 }: Meeting,
                                 i: number
                             ) => {
@@ -146,7 +148,10 @@ function MapTab({ meetingsByDay, focusSection }: MabTapProps) {
                                         start={start}
                                         end={end}
                                         room={room}
-                                        overlap={overlap!}
+                                        hasLocationData={
+                                            latitude != null &&
+                                            longitude != null
+                                        }
                                         focusSection={focusSection}
                                     />
                                 );
diff --git a/frontend/plan/components/meetUtil.tsx b/frontend/plan/components/meetUtil.tsx
index 8344516f..a4fbf8f6 100644
--- a/frontend/plan/components/meetUtil.tsx
+++ b/frontend/plan/components/meetUtil.tsx
@@ -134,7 +134,7 @@ export const getTimeString = (meetings: Meeting[]) => {
     });
 
     let daySet = "";
-    Object.values(Day).forEach((day) => {
+    ["M", "T", "W", "R", "F", "S", "U"].forEach((day) => {
         times[maxrange].forEach((d) => {
             if (d === day) {
                 daySet += day;
@@ -145,4 +145,4 @@ export const getTimeString = (meetings: Meeting[]) => {
     return `${intToTime(parseFloat(maxrange.split("-")[0]))}-${intToTime(
         parseFloat(maxrange.split("-")[1])
     )} ${daySet}`;
-};
\ No newline at end of file
+};
diff --git a/frontend/plan/constants/constants.js b/frontend/plan/constants/constants.js
index 3eb17d64..b4d23878 100644
--- a/frontend/plan/constants/constants.js
+++ b/frontend/plan/constants/constants.js
@@ -6,6 +6,4 @@ export const DAYS_TO_DAYSTRINGS = {
     W: "Wednesday",
     R: "Thursday",
     F: "Friday",
-    S: "Saturday",
-    U: "Sunday",
 };
diff --git a/frontend/plan/pages/index.tsx b/frontend/plan/pages/index.tsx
index a285140d..bc8193b4 100644
--- a/frontend/plan/pages/index.tsx
+++ b/frontend/plan/pages/index.tsx
@@ -68,13 +68,13 @@ const CustomTabs = styled(Tabs)`
     }
 `;
 
-const CartTab = styled.a<{ active: boolean }>`
+const CartTab = styled.a<{ active: string }>`
     display: inline-flex;
     font-weight: bold;
     margin-bottom: 0.5rem;
     cursor: pointer;
     margin-right: 1rem;
-    color: ${(props) => (props.active ? "black" : "gray")};
+    color: ${(props) => (props.active === "true" ? "black" : "gray")};
 `;
 
 const Box = styled.div`
@@ -441,7 +441,9 @@ function Index() {
                                             <CartTab
                                                 key={item}
                                                 href={`/#${item}`}
-                                                active={selectedTab === item}
+                                                active={(
+                                                    selectedTab === item
+                                                ).toString()}
                                                 onClick={() =>
                                                     setSelectedTab(item)
                                                 }
diff --git a/frontend/plan/types.ts b/frontend/plan/types.ts
index cab43fe6..9d036593 100644
--- a/frontend/plan/types.ts
+++ b/frontend/plan/types.ts
@@ -42,12 +42,17 @@ export interface CUFilter {
     1.5: boolean;
 }
 
-export enum Day {
+export type Day = Weekdays | Weekends;
+
+export enum Weekdays {
     M = "M",
     T = "T",
     W = "W",
     R = "R",
     F = "F",
+} 
+
+export enum Weekends {
     S = "S",
     U = "U",
 }