-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create locations pages and map #1613
base: john/20024-zod-poc
Are you sure you want to change the base?
Changes from all commits
f023bab
5330164
a1f2ec5
5bfcfbe
f189e00
a50c99c
fe0c935
0677f92
95eafb3
cfb017e
ad02f4b
be873eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
"use client"; | ||
import { notFound } from "next/navigation"; | ||
import Row from "react-bootstrap/Row"; | ||
import Col from "react-bootstrap/Col"; | ||
import AppBreadCrumb from "@/components/AppBreadCrumb"; | ||
import DataCard from "@/components/DataCard"; | ||
import LocationMapCard from "@/components/LocationMapCard"; | ||
import { useQuery } from "@/utils/graphql"; | ||
import { GET_LOCATION } from "@/queries/location"; | ||
import { locationSchema } from "@/schema/locationSchema"; | ||
import { locationCardColumns } from "@/configs/locationDataCard"; | ||
|
||
const typename = "atd_txdot_locations"; | ||
|
||
export default function LocationDetailsPage({ | ||
params, | ||
}: { | ||
params: { location_id: string }; | ||
}) { | ||
const locationId = params.location_id; | ||
|
||
const { data, error } = useQuery({ | ||
query: locationId ? GET_LOCATION : null, | ||
variables: { locationId }, | ||
schema: locationSchema, | ||
typename, | ||
}); | ||
|
||
if (error) { | ||
console.error(error); | ||
} | ||
|
||
if (!data) { | ||
// todo: loading spinner (would be nice to use a spinner inside cards) | ||
return; | ||
} | ||
|
||
if (data.length === 0) { | ||
// 404 | ||
notFound(); | ||
} | ||
|
||
const location = data[0]; | ||
|
||
return ( | ||
<> | ||
<AppBreadCrumb /> | ||
<span className="fs-2">{location.description}</span> | ||
<Row> | ||
<Col sm={12} md={8} className="mb-3"> | ||
<LocationMapCard location={location} /> | ||
</Col> | ||
<Col sm={12} md={4} className="mb-3"> | ||
<DataCard | ||
columns={locationCardColumns} | ||
isValidating={false} | ||
mutation="" | ||
title="Details" | ||
onSaveCallback={() => Promise.resolve()} | ||
record={location} | ||
/> | ||
</Col> | ||
</Row> | ||
</> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,28 @@ | ||
"use client"; | ||
import Card from "react-bootstrap/Card"; | ||
import AppBreadCrumb from "@/components/AppBreadCrumb"; | ||
import { locationsListViewColumns } from "@/configs/locationsListViewColumns"; | ||
import { locationsListViewQueryConfig } from "@/configs/locationsListViewTable"; | ||
import TableWrapper from "@/components/TableWrapper"; | ||
import { locationsListSchema } from "@/schema/locationsList"; | ||
|
||
const localStorageKey = "locationsListViewQueryConfig"; | ||
|
||
export default function Locations() { | ||
return <h1>This is the locations page</h1>; | ||
} | ||
|
||
return ( | ||
<> | ||
<AppBreadCrumb /> | ||
<Card className="mx-3 mb-3"> | ||
<Card.Header className="fs-5 fw-bold">Locations</Card.Header> | ||
<Card.Body> | ||
<TableWrapper | ||
columns={locationsListViewColumns} | ||
initialQueryConfig={locationsListViewQueryConfig} | ||
localStorageKey={localStorageKey} | ||
schema={locationsListSchema} | ||
/> | ||
</Card.Body> | ||
</Card> | ||
</> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { useRef, useMemo } from "react"; | ||
import MapGL, { | ||
FullscreenControl, | ||
NavigationControl, | ||
MapRef, | ||
Source, | ||
Layer, | ||
} from "react-map-gl"; | ||
import { center } from "@turf/center"; | ||
import { DEFAULT_MAP_PAN_ZOOM, DEFAULT_MAP_PARAMS } from "@/configs/map"; | ||
import "mapbox-gl/dist/mapbox-gl.css"; | ||
import { MapAerialSourceAndLayer } from "./MapAerialSourceAndLayer"; | ||
import { MultiPolygon } from "@/types/geojson"; | ||
import { LineLayerSpecification } from "mapbox-gl"; | ||
|
||
interface LocationMapProps { | ||
polygon: MultiPolygon; | ||
locationId: string; | ||
} | ||
|
||
const polygonLayer: LineLayerSpecification = { | ||
id: "location-polygon", | ||
source: "location-polygon", | ||
type: "line", | ||
paint: { | ||
"line-color": "orange", | ||
"line-width": 4, | ||
}, | ||
}; | ||
|
||
const usePolygonFeature = (polygon: MultiPolygon, locationId: string) => | ||
useMemo( | ||
() => [ | ||
{ | ||
type: "Feature", | ||
properties: { | ||
id: locationId, | ||
}, | ||
geometry: polygon, | ||
}, | ||
center(polygon), | ||
], | ||
[polygon, locationId] | ||
); | ||
|
||
/** | ||
* Map component which renders an editable point marker | ||
*/ | ||
export const LocationMap = ({ polygon, locationId }: LocationMapProps) => { | ||
const mapRef = useRef<MapRef | null>(null); | ||
const [polygonFeature, centerFeature] = usePolygonFeature( | ||
polygon, | ||
locationId | ||
); | ||
|
||
return ( | ||
<MapGL | ||
ref={mapRef} | ||
initialViewState={{ | ||
latitude: Number(centerFeature.geometry.coordinates[1]), | ||
longitude: Number(centerFeature.geometry.coordinates[0]), | ||
zoom: DEFAULT_MAP_PAN_ZOOM.zoom, | ||
}} | ||
{...DEFAULT_MAP_PARAMS} | ||
cooperativeGestures={true} | ||
// Resize the map canvas when parent row expands to fit crash | ||
onLoad={(e) => e.target.resize()} | ||
maxZoom={21} | ||
> | ||
<FullscreenControl position="top-left" /> | ||
<NavigationControl position="top-left" showCompass={false} /> | ||
<Source type="geojson" data={polygonFeature} id="location-polygon"> | ||
<Layer {...polygonLayer} /> | ||
</Source> | ||
{/* add nearmap raster source and style */} | ||
<MapAerialSourceAndLayer /> | ||
</MapGL> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import Card from "react-bootstrap/Card"; | ||
import { LocationMap } from "./LocationMap"; | ||
import { Location } from "@/types/locations"; | ||
/** | ||
* Card component that renders the crash map and edit controls | ||
*/ | ||
export default function LocationMapCard({ location }: { location: Location }) { | ||
return ( | ||
<Card> | ||
<Card.Header>Location</Card.Header> | ||
<Card.Body className="p-1 crash-header-card-body"> | ||
{location.geometry && ( | ||
<LocationMap | ||
polygon={location.geometry} | ||
locationId={location.location_id} | ||
/> | ||
)} | ||
</Card.Body> | ||
</Card> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,7 @@ import ListGroup from "react-bootstrap/ListGroup"; | |
import { | ||
FaShieldHeart, | ||
FaGaugeHigh, | ||
FaMap, | ||
FaLocationDot, | ||
FaAngleRight, | ||
FaRightFromBracket, | ||
} from "react-icons/fa6"; | ||
|
@@ -110,7 +110,7 @@ export default function SidebarLayout({ children }: { children: ReactNode }) { | |
<SideBarListItem | ||
isCollapsed={isCollapsed} | ||
isCurrentPage={segments.includes("locations")} | ||
Icon={FaMap} | ||
Icon={FaLocationDot} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i changed the sidebar icon to the map marker, this feels a lot more intuitive to me than the folding map icon |
||
label="Locations" | ||
href="/locations" | ||
/> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { ColDataCardDef } from "@/types/types"; | ||
import { Location } from "@/types/locations"; | ||
|
||
export const locationColumns: { | ||
[name: string]: ColDataCardDef<Location>; | ||
} = { | ||
location_id: { | ||
name: "location_id", | ||
label: "Location ID", | ||
}, | ||
// todo: need to support non-lookup table relationships | ||
// cr3_crash_count: { | ||
// name: "cr3_crash_count", | ||
// relationshipName: "locations_list_view" | ||
// } | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the current data cards can render relationships to lookup tables, but there no is mechanism to render card values through other relationships, like the |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { locationColumns } from "./locationColumns"; | ||
|
||
export const locationCardColumns = [locationColumns.location_id]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this feels a bit silly but i'm matching the pattern i used to set up the crash data card column arrays 🤷 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import Link from "next/link"; | ||
import { ColDataCardDef } from "@/types/types"; | ||
import { LocationsListLocation } from "@/types/locationsList"; | ||
import { renderNumber } from "@/utils/formHelpers"; | ||
|
||
export const locationsListViewColumns: ColDataCardDef<LocationsListLocation>[] = | ||
[ | ||
{ | ||
name: "location_id", | ||
label: "Location ID", | ||
sortable: true, | ||
valueRenderer: (record: LocationsListLocation) => ( | ||
<Link href={`/locations/${record.location_id}`}> | ||
{record.location_id} | ||
</Link> | ||
), | ||
}, | ||
{ | ||
name: "description", | ||
label: "Location", | ||
sortable: true, | ||
}, | ||
{ | ||
name: "cr3_crash_count", | ||
label: "CR3 crashes", | ||
sortable: true, | ||
valueFormatter: renderNumber, | ||
}, | ||
{ | ||
name: "non_cr3_crash_count", | ||
label: "Non-CR3 crashes", | ||
sortable: true, | ||
valueFormatter: renderNumber, | ||
}, | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.