Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions backend/degree/migrations/0004_alter_degree_program.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 5.0.2 on 2026-02-15 19:17

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("degree", "0003_alter_degree_program_alter_fulfillment_legal_and_more"),
]

operations = [
migrations.AlterField(
model_name="degree",
name="program",
field=models.CharField(
choices=[
("EU_BSE", "Engineering BSE"),
("EU_BAS", "Engineering BAS"),
("AU_BA", "College BA"),
("WU_BS", "Wharton BS"),
("NU_BSN", "Nursing BSN"),
],
help_text="\nThe program code for this degree, e.g., EU_BSE\n",
max_length=16,
),
),
]
226 changes: 190 additions & 36 deletions frontend/plan/components/map/Map.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import React, { useEffect } from "react";
import { MapContainer, TileLayer, useMap } from "react-leaflet";
import Marker from "../map/Marker";
import React, { useEffect, useMemo, useRef, useState } from "react";
import MapGL, {
Layer,
NavigationControl,
Popup,
Source,
type LayerProps,
type MapRef,
} from "react-map-gl/mapbox";
import type { FeatureCollection, Point } from "geojson";
import { Location } from "../../types";

interface MapProps {
locations: Location[];
zoom: number;
viewKey?: string;
}

function toRadians(degrees: number): number {
Expand All @@ -20,6 +28,7 @@ function toDegrees(radians: number): number {
function getGeographicCenter(
locations: Location[]
): [number, number] {
if (!locations.length) return [39.9515, -75.191];
let x = 0;
let y = 0;
let z = 0;
Expand Down Expand Up @@ -79,48 +88,193 @@ function separateOverlappingPoints(points: Location[], offset = 0.0001) {
return adjustedPoints;
}

interface InnerMapProps {
locations: Location[];
center: [number, number]
}
function Map({ locations, zoom, viewKey }: MapProps) {
const mapRef = useRef<MapRef | null>(null);
const mapboxToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN;
const mapboxStyleId = process.env.NEXT_PUBLIC_MAPBOX_STYLE_ID || "mapbox/streets-v12";
const [cursor, setCursor] = useState<string>("");
const [autoCenter, setAutoCenter] = useState(true);
const [selected, setSelected] = useState<{
longitude: number;
latitude: number;
id?: string;
room?: string;
start?: number;
end?: number;
color?: string;
} | null>(null);

const mapStyle = useMemo(() => {
if (mapboxStyleId.startsWith("mapbox://")) return mapboxStyleId;
return `mapbox://styles/${mapboxStyleId}`;
}, [mapboxStyleId]);

const center = useMemo(() => getGeographicCenter(locations), [locations]);
const points = useMemo(() => separateOverlappingPoints(locations), [locations]);
const markerGeoJson = useMemo<
FeatureCollection<Point, { color?: string; id?: string; room?: string; start?: number; end?: number }>
>(() => {
return {
type: "FeatureCollection",
features: points.map((p) => ({
type: "Feature",
properties: {
color: p.color,
id: p.id,
room: p.room,
start: p.start,
end: p.end,
},
geometry: { type: "Point", coordinates: [p.lng, p.lat] },
})),
};
}, [points]);

const markerLayer = useMemo<LayerProps>(
() => ({
id: "pcp-course-markers",
type: "circle",
paint: {
"circle-radius": 6,
"circle-color": ["coalesce", ["get", "color"], "#878ED8"],
"circle-stroke-color": "rgba(0,0,0,0.35)",
"circle-stroke-width": 2,
},
}),
[]
);


// need inner child component to use useMap hook to run on client
function InnerMap({ locations, center } :InnerMapProps) {
const map = useMap();
const formatTime = (t?: number) => {
if (t == null) return "";
const hours24 = Math.floor(t);
const minutes = Math.round((t % 1) * 100);
const period = hours24 >= 12 ? "PM" : "AM";
const hours12 = hours24 % 12 === 0 ? 12 : hours24 % 12;
return `${hours12}:${minutes.toString().padStart(2, "0")} ${period}`;
};

// When the day/view changes, re-enable auto-centering.
useEffect(() => {
map.flyTo({ lat: center[0], lng: center[1]})
}, [center[0], center[1]])
setAutoCenter(true);
setSelected(null);
}, [viewKey]);

return (
<>
<TileLayer
// @ts-ignore
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{separateOverlappingPoints(locations).map(({ lat, lng, color }, i) => (
<Marker key={i} lat={lat} lng={lng} color={color}/>
))}
</>
)
useEffect(() => {
if (!mapRef.current || !autoCenter) return;
mapRef.current.flyTo({
center: [center[1], center[0]],
zoom,
essential: true,
});
}, [autoCenter, center, zoom]);

}
if (!mapboxToken) {
return (
<div
style={{
height: "100%",
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#6b7280",
fontSize: "0.9rem",
background: "#f9fafb",
borderRadius: 8,
}}
>
Missing `NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN`
</div>
);
}

function Map({ locations, zoom }: MapProps) {
const center = getGeographicCenter(locations);

return (
<MapContainer
// @ts-ignore
center={center}
zoom={zoom}
zoomControl={false}
scrollWheelZoom={true}
<MapGL
ref={mapRef}
mapboxAccessToken={mapboxToken}
mapStyle={mapStyle}
initialViewState={{
latitude: center[0],
longitude: center[1],
zoom,
pitch: 0,
bearing: 0,
}}
style={{ height: "100%", width: "100%" }}
attributionControl
dragRotate
touchPitch
maxPitch={70}
interactiveLayerIds={["pcp-course-markers"]}
cursor={cursor}
onDragStart={() => setAutoCenter(false)}
onZoomStart={() => setAutoCenter(false)}
onRotateStart={() => setAutoCenter(false)}
onPitchStart={() => setAutoCenter(false)}
onMouseMove={(e) => {
const hovering = (e.features?.length || 0) > 0;
setCursor(hovering ? "pointer" : "");
}}
onClick={(e) => {
const f = e.features?.[0];
if (!f || f.geometry.type !== "Point") {
setSelected(null);
return;
}
const [lng, lat] = f.geometry.coordinates as [number, number];
const props = (f.properties || {}) as Record<string, unknown>;

// Center the map on the clicked marker (keep current zoom).
setAutoCenter(false);
mapRef.current?.flyTo({
center: [lng, lat],
essential: true,
});

setSelected({
longitude: lng,
latitude: lat,
id: typeof props.id === "string" ? props.id : undefined,
room: typeof props.room === "string" ? props.room : undefined,
start: typeof props.start === "number" ? props.start : undefined,
end: typeof props.end === "number" ? props.end : undefined,
color: typeof props.color === "string" ? props.color : undefined,
});
}}
>
<InnerMap locations={locations} center={center}/>
</MapContainer>
<NavigationControl showCompass showZoom visualizePitch position="top-left" />

<Source id="pcp-course-markers-source" type="geojson" data={markerGeoJson}>
<Layer {...markerLayer} />
</Source>

{selected && (
<Popup
longitude={selected.longitude}
latitude={selected.latitude}
anchor="top"
closeButton
closeOnClick={false}
onClose={() => setSelected(null)}
maxWidth="260px"
>
<div style={{ fontSize: "0.85rem", lineHeight: 1.25 }}>
{selected.id && (
<div style={{ fontWeight: 700, marginBottom: 4 }}>
{selected.id.replace(/-/g, " ")}
</div>
)}
{(selected.start != null || selected.end != null) && (
<div style={{ marginBottom: 2 }}>
{formatTime(selected.start)}{selected.end != null ? `–${formatTime(selected.end)}` : ""}
</div>
)}
{selected.room && <div>{selected.room}</div>}
</div>
</Popup>
)}
</MapGL>
);
};

Expand Down
7 changes: 6 additions & 1 deletion frontend/plan/components/map/MapTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import MapCourseItem from "./MapCourseItem";
import { scheduleContainsSection } from "../meetUtil";
import { DAYS_TO_DAYSTRINGS } from "../../constants/constants";
import { Section, Meeting, Day, Weekdays } from "../../types";
import "leaflet/dist/leaflet.css";
import "mapbox-gl/dist/mapbox-gl.css";
import { ThunkDispatch } from "redux-thunk";
import { fetchCourseDetails } from "../../actions";

Expand Down Expand Up @@ -128,13 +128,18 @@ function MapTab({
lat: meeting.latitude,
lng: meeting.longitude,
color: meeting.color,
id: meeting.id,
room: meeting.room,
start: meeting.start,
end: meeting.end,
}))
.filter(
(locData) =>
locData.lat != null &&
locData.lng != null
)}
zoom={14}
viewKey={selectedDay}
/>
</MapContainer>
<MapCourseItemcontainer>
Expand Down
8 changes: 4 additions & 4 deletions frontend/plan/components/map/Marker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ const Marker = ({ color = "#878ED8", lat, lng }) => {
const icon = divIcon({
html: `
<svg
width="20"
height="28"
width="14"
height="20"
viewBox="0 0 20 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -19,8 +19,8 @@ const Marker = ({ color = "#878ED8", lat, lng }) => {
</svg>
`,
className: "svg-icon",
iconSize: [24, 40],
iconAnchor: [12, 40],
iconSize: [18, 30],
iconAnchor: [9, 30],
});

return <MarkerLeaflet position={[lat, lng]} icon={icon} />;
Expand Down
2 changes: 1 addition & 1 deletion frontend/plan/components/modals/MapModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import styled from "styled-components";
import Map from "../map/Map";
import "leaflet/dist/leaflet.css";
import "mapbox-gl/dist/mapbox-gl.css";

interface MapModalProps {
lat: number;
Expand Down
2 changes: 2 additions & 0 deletions frontend/plan/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"ics": "^2.37.0",
"leaflet": "^1.9.4",
"libphonenumber-js": "^1.10.57",
"mapbox-gl": "^3.18.1",
"next": "13.2.1",
"next-transpile-modules": "^3.3.0",
"pcx-shared-components": "0.1.0",
Expand All @@ -42,6 +43,7 @@
"react-ga": "^2.7.0",
"react-hook-form": "^7.51.1",
"react-leaflet": "^4.2.1",
"react-map-gl": "^8.1.0",
"react-redux": "^7.2.9",
"react-spring": "^8.0.27",
"react-swipeable-views": "^0.13.3",
Expand Down
2 changes: 1 addition & 1 deletion frontend/plan/tsconfig.tsbuildinfo

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions frontend/plan/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,5 +262,9 @@ export type FilterType =
lat: number;
lng: number;
color?: string;
id?: string;
room?: string;
start?: number;
end?: number;
}

Loading
Loading