Skip to content

Commit 712614d

Browse files
Feature/make map prettier (#781)
* Map uses CARTO now, looks better * Add map marker popups and click-to-center Markers now open a popup with course details and both marker and list clicks re-center the Leaflet map for easier navigation. * Refine map marker helpers Clean up marker default color handling and list click parsing, and simplify the map flyTo effect. --------- Co-authored-by: ronitkapoor0106 <ronitkapoor0106@gmail.com>
1 parent b814196 commit 712614d

6 files changed

Lines changed: 127 additions & 22 deletions

File tree

frontend/plan/components/map/Map.tsx

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { Location } from "../../types";
66
interface MapProps {
77
locations: Location[];
88
zoom: number;
9+
focusedLocation?: { lat: number; lng: number } | null;
10+
onMarkerClick?: (loc: { lat: number; lng: number }) => void;
911
}
1012

1113
function toRadians(degrees: number): number {
@@ -82,32 +84,47 @@ function separateOverlappingPoints(points: Location[], offset = 0.0001) {
8284
interface InnerMapProps {
8385
locations: Location[];
8486
center: [number, number]
87+
focusedLocation?: { lat: number; lng: number } | null;
88+
onMarkerClick?: (loc: { lat: number; lng: number }) => void;
8589
}
8690

8791
// need inner child component to use useMap hook to run on client
88-
function InnerMap({ locations, center } :InnerMapProps) {
92+
function InnerMap({ locations, center, focusedLocation, onMarkerClick } :InnerMapProps) {
8993
const map = useMap();
9094

9195
useEffect(() => {
92-
map.flyTo({ lat: center[0], lng: center[1]})
93-
}, [center[0], center[1]])
96+
if (!map) return;
97+
const target = focusedLocation ?? { lat: center[0], lng: center[1] };
98+
map.flyTo(target);
99+
}, [center[0], center[1], focusedLocation?.lat, focusedLocation?.lng]);
94100

95101
return (
96102
<>
97103
<TileLayer
98104
// @ts-ignore
99-
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
100-
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
105+
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
106+
url="https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png"
107+
maxZoom={19}
101108
/>
102-
{separateOverlappingPoints(locations).map(({ lat, lng, color }, i) => (
103-
<Marker key={i} lat={lat} lng={lng} color={color}/>
109+
{separateOverlappingPoints(locations).map(({ lat, lng, color, id, start, end, room }, i) => (
110+
<Marker
111+
key={i}
112+
lat={lat}
113+
lng={lng}
114+
color={color}
115+
id={id}
116+
start={start}
117+
end={end}
118+
room={room}
119+
onClick={onMarkerClick ? () => onMarkerClick({ lat, lng }) : undefined}
120+
/>
104121
))}
105122
</>
106123
)
107124

108125
}
109126

110-
function Map({ locations, zoom }: MapProps) {
127+
function Map({ locations, zoom, focusedLocation, onMarkerClick }: MapProps) {
111128
const center = getGeographicCenter(locations);
112129

113130
return (
@@ -119,7 +136,12 @@ function Map({ locations, zoom }: MapProps) {
119136
scrollWheelZoom={true}
120137
style={{ height: "100%", width: "100%" }}
121138
>
122-
<InnerMap locations={locations} center={center}/>
139+
<InnerMap
140+
locations={locations}
141+
center={center}
142+
focusedLocation={focusedLocation}
143+
onMarkerClick={onMarkerClick}
144+
/>
123145
</MapContainer>
124146
);
125147
};

frontend/plan/components/map/MapCourseItem.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,10 @@ interface CartSectionProps {
100100
end: number;
101101
room: string;
102102
hasLocationData: boolean;
103+
lat?: number | null;
104+
lng?: number | null;
103105
focusSection: (id: string) => void;
106+
focusLocation?: (loc: { lat: number; lng: number }) => void;
104107
}
105108

106109
function MapCourseItem({
@@ -110,7 +113,10 @@ function MapCourseItem({
110113
end,
111114
room,
112115
hasLocationData,
116+
lat,
117+
lng,
113118
focusSection,
119+
focusLocation,
114120
}: CartSectionProps) {
115121
return (
116122
<CourseCartItem
@@ -120,8 +126,11 @@ function MapCourseItem({
120126
$isMobile={isMobile}
121127
$hasLocationData={hasLocationData}
122128
onClick={() => {
123-
const split = id.split("-");
124-
focusSection(`${split[0]}-${split[1]}`);
129+
const [courseDepartment, courseCode] = id.split("-");
130+
focusSection(`${courseDepartment}-${courseCode}`);
131+
if (focusLocation && lat != null && lng != null) {
132+
focusLocation({ lat, lng });
133+
}
125134
}}
126135
>
127136
<Dot $color={color} />

frontend/plan/components/map/MapTab.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from "react";
1+
import React, { useEffect, useState } from "react";
22
import { connect } from "react-redux";
33
import dynamic from "next/dynamic";
44
import styled from "styled-components";
@@ -49,9 +49,16 @@ const Box = styled.section<{ $length: number }>`
4949

5050
const MapContainer = styled.div`
5151
height: 40%;
52-
margin-right: 8px;
53-
margin-left: 8px;
54-
margin-top: 5px;
52+
margin: 8px 8px 12px;
53+
border-radius: 8px;
54+
overflow: hidden;
55+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
56+
57+
.leaflet-control-attribution {
58+
font-size: 9px;
59+
opacity: 0.6;
60+
padding: 1px 4px;
61+
}
5562
`;
5663

5764
const MapCourseItemcontainer = styled.div`
@@ -106,8 +113,13 @@ function MapTab({
106113
focusSection,
107114
}: MabTapProps) {
108115
const [selectedDay, setSelectedDay] = useState<Day>(Weekdays.M);
116+
const [focusedLocation, setFocusedLocation] = useState<{ lat: number; lng: number } | null>(null);
109117
const meeetingsForDay = meetingsByDay[selectedDay];
110118

119+
useEffect(() => {
120+
setFocusedLocation(null);
121+
}, [selectedDay]);
122+
111123
return (
112124
<Box $length={meeetingsForDay.length} id="cart">
113125
<MapRadio
@@ -128,13 +140,19 @@ function MapTab({
128140
lat: meeting.latitude,
129141
lng: meeting.longitude,
130142
color: meeting.color,
143+
id: meeting.id,
144+
start: meeting.start,
145+
end: meeting.end,
146+
room: meeting.room,
131147
}))
132148
.filter(
133149
(locData) =>
134150
locData.lat != null &&
135151
locData.lng != null
136152
)}
137153
zoom={14}
154+
focusedLocation={focusedLocation}
155+
onMarkerClick={setFocusedLocation}
138156
/>
139157
</MapContainer>
140158
<MapCourseItemcontainer>
@@ -163,7 +181,10 @@ function MapTab({
163181
latitude != null &&
164182
longitude != null
165183
}
184+
lat={latitude}
185+
lng={longitude}
166186
focusSection={focusSection}
187+
focusLocation={setFocusedLocation}
167188
/>
168189
);
169190
}
Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
import React from "react";
2-
import { Marker as MarkerLeaflet } from "react-leaflet";
2+
import { Marker as MarkerLeaflet, Popup } from "react-leaflet";
33
import { divIcon } from "leaflet";
44

5-
const Marker = ({ color = "#878ED8", lat, lng }) => {
5+
const DEFAULT_MARKER_COLOR = "#878ED8";
6+
7+
const formatTime = (t) => {
8+
if (t == null) return "";
9+
let hour = Math.floor(t % 12);
10+
const min = Math.round((t % 1) * 100);
11+
if (hour === 0) hour = 12;
12+
const minStr = min === 0 ? "00" : min.toString().padStart(2, "0");
13+
return `${hour}:${minStr}`;
14+
};
15+
16+
const Marker = ({ color = DEFAULT_MARKER_COLOR, lat, lng, id, start, end, room, onClick }) => {
617
const icon = divIcon({
718
html: `
819
<svg
9-
width="20"
10-
height="28"
20+
width="14"
21+
height="20"
1122
viewBox="0 0 20 28"
1223
fill="none"
1324
xmlns="http://www.w3.org/2000/svg"
@@ -19,11 +30,40 @@ const Marker = ({ color = "#878ED8", lat, lng }) => {
1930
</svg>
2031
`,
2132
className: "svg-icon",
22-
iconSize: [24, 40],
23-
iconAnchor: [12, 40],
33+
iconSize: [18, 30],
34+
iconAnchor: [9, 30],
2435
});
2536

26-
return <MarkerLeaflet position={[lat, lng]} icon={icon} />;
37+
return (
38+
<MarkerLeaflet
39+
position={[lat, lng]}
40+
icon={icon}
41+
eventHandlers={
42+
onClick
43+
? {
44+
click: onClick,
45+
}
46+
: undefined
47+
}
48+
>
49+
<Popup>
50+
<div style={{ fontSize: "0.85rem", lineHeight: 1.25 }}>
51+
{id && (
52+
<div style={{ fontWeight: 700, marginBottom: 4 }}>
53+
{id.replace(/-/g, " ")}
54+
</div>
55+
)}
56+
{(start != null || end != null) && (
57+
<div style={{ marginBottom: 2 }}>
58+
{formatTime(start)}
59+
{end != null ? `-${formatTime(end)}` : ""}
60+
</div>
61+
)}
62+
{room && <div>{room}</div>}
63+
</div>
64+
</Popup>
65+
</MarkerLeaflet>
66+
);
2767
};
2868

2969
export default React.memo(Marker);

frontend/plan/components/modals/MapModal.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ const MapModal = ({ lat, lng, room, title }: MapModalProps) => {
2020
height: 100%;
2121
width: 70%;
2222
margin-right: 10px;
23+
border-radius: 8px;
24+
overflow: hidden;
25+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
26+
27+
.leaflet-control-attribution {
28+
font-size: 9px;
29+
opacity: 0.6;
30+
padding: 1px 4px;
31+
}
2332
`;
2433

2534
const MapInfoContainer = styled.ul`

frontend/plan/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,5 +262,9 @@ export type FilterType =
262262
lat: number;
263263
lng: number;
264264
color?: string;
265+
id?: string;
266+
start?: number;
267+
end?: number;
268+
room?: string;
265269
}
266270

0 commit comments

Comments
 (0)