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
3 changes: 2 additions & 1 deletion src/app/api/schools/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export async function GET(req: NextRequest) {
region: schools.region,
gateway: schools.gateway,
})
.from(schools);
.from(schools)
.orderBy(schools.name);

// Only filter if gateway=true is explicitly passed
const allSchools = await (isGateway
Expand Down
45 changes: 6 additions & 39 deletions src/app/schools/[name]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { SchoolProfileSkeleton } from "@/components/skeletons/SchoolProfileSkeleton";
import { Skeleton } from "@/components/ui/skeleton";
import { MapPlacer } from "@/components/ui/mapPlacer";
import { SchoolLocationEditor } from "@/components/SchoolLocationEditor";
import { SchoolInfoRow } from "@/components/SchoolInfoRow";
import { StatCard } from "@/components/ui/stat-card";
import { ENTITY_CONFIG } from "@/lib/entity-config";
Expand Down Expand Up @@ -52,11 +52,6 @@ type SchoolData = {
schoolType: string;
};

type MapCoordinates = {
latitude: number | null;
longitude: number | null;
};

type ProjectRow = EditableProjectRow;

export default function SchoolProfilePage() {
Expand All @@ -66,7 +61,7 @@ export default function SchoolProfilePage() {

const [schoolData, setSchoolData] = useState<SchoolData | null>(null);
const [prevYearData, setPrevYearData] = useState<SchoolData | null>(null);
const [coordinates, setCoordinates] = useState<MapCoordinates | null>(null);

const [year, setYear] = useState<number | null>(null);
const [projects, setProjects] = useState<ProjectRow[]>([]);
const [editingName, setEditingName] = useState(false);
Expand Down Expand Up @@ -397,40 +392,12 @@ export default function SchoolProfilePage() {

{/* School location map */}
<div className="rounded-lg space-y-4">
<h2 className="text-xl font-semibold mb-4 text-foreground">
<h2 className="text-xl font-semibold text-foreground">
School Location
</h2>
<div className="h-80 rounded-lg overflow-hidden border border-border">
<MapPlacer
schoolId={schoolName}
schoolName={schoolData.name}
onCoordinatesLoaded={setCoordinates}
/>
</div>
<div className="mt-3 text-sm text-muted-foreground flex justify-between items-center">
<div className="flex items-center gap-1.5">
{coordinates &&
coordinates.latitude !== null &&
coordinates.longitude !== null && (
<div className="bg-muted text-black px-2 rounded border">
<span>
Coordinates:{" "}
{coordinates.latitude.toFixed(6)},{" "}
{coordinates.longitude.toFixed(6)}
</span>
</div>
)}
</div>
<div>
{/* TO DO: Replace with actual dates from db */}
Last Updated:{" "}
{new Date().toLocaleDateString("en-US", {
month: "2-digit",
day: "2-digit",
year: "numeric",
})}
</div>
</div>
<SchoolLocationEditor
fixedSchool={{ name: schoolData.name }}
/>
</div>

{/* Editable project data table */}
Expand Down
252 changes: 5 additions & 247 deletions src/app/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,16 @@

"use client";

import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/Combobox";
import YearsOfData, { YearsOfDataHandle } from "@/components/YearsOfData";
import { Map, MapMarker, MarkerContent, useMap } from "@/components/ui/map";
import { toast } from "sonner";
import GatewaySchools, {
GatewaySchoolsHandle,
} from "@/components/GatewaySchools";
import { standardize } from "@/lib/string-standardize";
import {
SchoolLocationEditor,
type SchoolLocationEditorHandle,
} from "@/components/SchoolLocationEditor";
import { useRouter } from "next/navigation";
import { useUnsavedChanges } from "@/components/UnsavedChangesContext";
import {
Expand Down Expand Up @@ -190,238 +183,3 @@ export default function Settings() {
</div>
);
}

// Helper: registers click events on the map for the settings editor
function SettingsMapClickHandler({
onMapClick,
}: {
onMapClick: (lng: number, lat: number) => void;
}) {
const { map } = useMap();

useEffect(() => {
if (!map) return;

const handleClick = (e: { lngLat: { lng: number; lat: number } }) => {
onMapClick(e.lngLat.lng, e.lngLat.lat);
};

map.on("click", handleClick);
map.getCanvas().style.cursor = "crosshair";

return () => {
map.off("click", handleClick);
map.getCanvas().style.cursor = "";
};
}, [map, onMapClick]);

return null;
}

interface SchoolEntry {
id: number;
name: string;
latitude: number | null;
longitude: number | null;
}

export interface SchoolLocationEditorHandle {
save: () => Promise<void>;
discard: () => void;
}

const SchoolLocationEditor = forwardRef<
SchoolLocationEditorHandle,
{ onUnsavedChange: () => void }
>(function SchoolLocationEditor({ onUnsavedChange }, ref) {
const [schools, setSchools] = useState<SchoolEntry[]>([]);
const [selectedSchoolId, setSelectedSchoolId] = useState("");
const [editing, setEditing] = useState(false);
const [newPin, setNewPin] = useState<{
latitude: number;
longitude: number;
} | null>(null);

const [mounted, setMounted] = useState(false);

useEffect(() => {
setMounted(true);
}, []);

useEffect(() => {
fetch("/api/schools?list=true")
.then((res) => res.json())
.then((data) => setSchools(data));
}, []);

const selectedSchool = schools.find(
(s) => String(s.id) === selectedSchoolId,
);

const schoolOptions = schools.map((s) => ({
value: String(s.id),
label: s.name,
}));

const handleSchoolChange = (value: string) => {
setSelectedSchoolId(value);
setEditing(true);
setNewPin(null);
};

const handleMapClick = useCallback(
async (long: number, lat: number) => {
let validLocation: boolean = false;

try {
const res = await fetch(
`/api/coordinate-to-region/?lat=${lat}&long=${long}`,
);
const data = await res.json();

// Location is only in MA if it has a region
if (res.ok && data.region) {
validLocation = true;
} else {
toast.error(
"A school's location must fall within Massachusetts.",
);
}
} catch {
toast.error("Error validating school location");
}

if (validLocation) {
setNewPin({ latitude: lat, longitude: long });
onUnsavedChange();
}
},
[onUnsavedChange],
);

const handleSave = async () => {
if (!newPin || !selectedSchool) return;

try {
const slugName = standardize(selectedSchool.name);
const response = await fetch(`/api/schools/${slugName}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
latitude: newPin.latitude,
longitude: newPin.longitude,
}),
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(
errorData.error || "Failed to update school location",
);
}

// Update local state
setSchools((prev) =>
prev.map((s) =>
s.id === selectedSchool.id
? {
...s,
latitude: newPin.latitude,
longitude: newPin.longitude,
}
: s,
),
);
setNewPin(null);
toast.success(`Location updated for ${selectedSchool.name}`);
} catch (err) {
const errorMsg =
err instanceof Error ? err.message : "Failed to save location";
toast.error(errorMsg);
}
};

const handleCancel = () => {
setNewPin(null);
};

useImperativeHandle(ref, () => ({
save: handleSave,
discard: handleCancel,
}));

const mapCenter: [number, number] =
selectedSchool?.longitude && selectedSchool?.latitude
? [selectedSchool?.longitude, selectedSchool?.latitude]
: [-72, 42.272];

return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="font-bold">School Locations</h3>
</div>
<div className="min-w-72 w-fit">
<Combobox
options={schoolOptions}
value={selectedSchoolId}
onChange={handleSchoolChange}
placeholder="Search for a school..."
/>
</div>

{selectedSchool && mounted && (
<div className="space-y-3">
<div className="h-80 rounded-lg overflow-hidden border border-gray-200 relative">
<Map
key={selectedSchool.id}
center={mapCenter}
zoom={12}
scrollZoom={true}
dragPan={true}
dragRotate={false}
doubleClickZoom={editing}
touchZoomRotate={editing}
>
{/* Current school location (red) */}
{selectedSchool.latitude &&
selectedSchool.longitude && (
<MapMarker
longitude={selectedSchool.longitude}
latitude={selectedSchool.latitude}
>
<MarkerContent>
<div className="flex h-4 w-4 items-center justify-center rounded-full bg-red-500/60 border-2 border-red-500 shadow-lg" />
</MarkerContent>
</MapMarker>
)}
{/* New pin (blue) */}
{newPin && (
<MapMarker
longitude={newPin.longitude}
latitude={newPin.latitude}
>
<MarkerContent>
<div className="flex h-4 w-4 items-center justify-center rounded-full bg-blue-500/60 border-2 border-blue-500 shadow-lg" />
</MarkerContent>
</MapMarker>
)}
{editing && (
<SettingsMapClickHandler
onMapClick={handleMapClick}
/>
)}
</Map>
</div>

<div className="text-sm">
{newPin ? (
<div className="bg-muted text-black px-2 rounded border">{`New location: ${newPin.latitude.toFixed(4)}, ${newPin.longitude.toFixed(4)}`}</div>
) : (
"Click on the map to set a new location"
)}
</div>
</div>
)}
</div>
);
});
Loading