Skip to content

Commit

Permalink
feat: improved live map UI
Browse files Browse the repository at this point in the history
  • Loading branch information
casperiv0 committed Jan 7, 2024
1 parent 7497791 commit 972d2fd
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 281 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -345,22 +345,24 @@ function ManageURLPopover(props: ManageURLPopoverProps) {
</Popover.Trigger>

<Popover.Content className="z-[999] p-4 bg-gray-200 rounded-md shadow-md dropdown-fade w-96 dark:bg-primary dark:border dark:border-secondary text-base font-normal">
<h3 className="text-xl font-semibold mb-3">{props.url ? t("editURL") : t("addURL")}</h3>

<div>
<TextField label={common("name")} value={name} onChange={(value) => setName(value)} />
<TextField
placeholder="http://my-host:my-port"
type="url"
label={common("url")}
value={url}
onChange={(value) => setUrl(value)}
/>

<Button type="button" onPress={handleSubmit} size="xs">
{props.url ? common("save") : t("addURL")}
</Button>
</div>
<form onSubmit={handleSubmit}>
<h3 className="text-xl font-semibold mb-3">{props.url ? t("editURL") : t("addURL")}</h3>

<div>
<TextField label={common("name")} value={name} onChange={(value) => setName(value)} />
<TextField
placeholder="http://my-host:my-port"
type="url"
label={common("url")}
value={url}
onChange={(value) => setUrl(value)}
/>

<Button type="submit" onPress={handleSubmit} size="xs">
{props.url ? common("save") : t("addURL")}
</Button>
</div>
</form>

<Popover.Arrow className="fill-primary" />
</Popover.Content>
Expand Down
72 changes: 35 additions & 37 deletions apps/client/src/components/dispatch/map/calls/active-map-calls.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
import * as React from "react";
import { createPortal } from "react-dom";
import { useTranslations } from "next-intl";
import type { Full911Call } from "state/dispatch/dispatch-state";
import type { Call911 } from "@snailycad/types";
import { Manage911CallModal } from "components/dispatch/active-calls/modals/manage-911-call-modal";
import { useListener } from "@casperiv/use-socket.io";
import { SocketEvents } from "@snailycad/config";
import { usePortal } from "@casperiv/useful";
import { CallItem } from "./call-item";
import { useCall911State } from "state/dispatch/call-911-state";
import { Accordion } from "@snailycad/ui";
import { useDispatchMapState } from "state/mapState";
import { useMarkerChange } from "./use-marker-change";

export interface MapCallProps {
hasMarker(callId: string): boolean;
setMarker(call: Full911Call, type: "remove" | "set"): void;
openItems: string[];
setOpenItems: React.Dispatch<React.SetStateAction<string[]>>;
}

export function ActiveMapCalls({ hasMarker, setOpenItems, openItems, setMarker }: MapCallProps) {
export function ActiveMapCalls() {
const t = useTranslations("Calls");
const calls911State = useCall911State();
const portalRef = usePortal("ActiveMapCalls");
const { handleMarkerChange } = useMarkerChange();
const { openCalls, setOpenCalls } = useDispatchMapState((state) => ({
openCalls: state.openCalls,
setOpenCalls: state.setOpenCalls,
}));
const callsWithPosition = React.useMemo(() => {
return calls911State.calls.filter(
(v) => v.gtaMapPosition || (v.position?.lat && v.position.lng),
);
}, [calls911State.calls]);

useListener(
SocketEvents.Create911Call,
Expand Down Expand Up @@ -57,31 +58,28 @@ export function ActiveMapCalls({ hasMarker, setOpenItems, openItems, setMarker }
);

return (
portalRef &&
createPortal(
<div
id="map-calls"
className="fixed z-[29] p-3 max-h-[88vh] thin-scrollbar top-20 left-4 w-80 rounded-md shadow bg-gray-50 dark:bg-tertiary dark:border dark:border-secondary dark:text-white overflow-y-auto"
>
<h1 className="text-xl font-semibold">{t("active911Calls")}</h1>
{calls911State.calls.length <= 0 ? (
<p>{t("no911Calls")}</p>
) : (
<Accordion value={openItems} onValueChange={setOpenItems} type="multiple">
{calls911State.calls.map((call) => {
return (
<CallItem hasMarker={hasMarker} setMarker={setMarker} key={call.id} call={call} />
);
})}
</Accordion>
)}
<div className="text-white overflow-y-auto">
{calls911State.calls.length <= 0 ? (
<p className="p-2">{t("no911Calls")}</p>
) : (
<Accordion value={openCalls} onValueChange={setOpenCalls} type="multiple">
{calls911State.calls.map((call) => {
return (
<CallItem
hasMarker={(callId) => callsWithPosition.some((c) => c.id === callId)}
setMarker={handleMarkerChange}
key={call.id}
call={call}
/>
);
})}
</Accordion>
)}

<Manage911CallModal
onClose={() => calls911State.setCurrentlySelectedCall(null)}
call={calls911State.currentlySelectedCall}
/>
</div>,
portalRef,
)
<Manage911CallModal
onClose={() => calls911State.setCurrentlySelectedCall(null)}
call={calls911State.currentlySelectedCall}
/>
</div>
);
}
5 changes: 3 additions & 2 deletions apps/client/src/components/dispatch/map/calls/call-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import {
} from "@snailycad/ui";
import { useModal } from "state/modalState";
import type { Full911Call } from "state/dispatch/dispatch-state";
import type { MapCallProps } from "./active-map-calls";
import { useTranslations } from "next-intl";
import { isUnitCombined } from "@snailycad/utils";
import { useCall911State } from "state/dispatch/call-911-state";
import { CallDescription } from "components/dispatch/active-calls/CallDescription";

interface CallItemProps extends Omit<MapCallProps, "toggledId" | "openItems" | "setOpenItems"> {
interface CallItemProps {
hasMarker(callId: string): boolean;
setMarker(call: Full911Call, type: "remove" | "set"): void;
call: Full911Call;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import * as React from "react";
import { icon as leafletIcon, type LeafletEvent } from "leaflet";
import useFetch from "lib/useFetch";
import { icon as leafletIcon } from "leaflet";
import { Marker, Popup, Tooltip, useMap } from "react-leaflet";
import type { Full911Call } from "state/dispatch/dispatch-state";
import { ActiveMapCalls } from "./active-map-calls";
import { convertToMap } from "lib/map/utils";
import { Button } from "@snailycad/ui";
import { useTranslations } from "next-intl";
import type { Put911CallByIdData } from "@snailycad/types/api";
import { useCall911State } from "state/dispatch/call-911-state";
import { CallDescription } from "components/dispatch/active-calls/CallDescription";
import { MapItem, useDispatchMapState } from "state/mapState";
import { useMarkerChange } from "./use-marker-change";

const CALL_ICON_SIZE = 30;

Expand All @@ -23,145 +20,79 @@ const CALL_ICON = leafletIcon({
});

export function RenderActiveCalls() {
const [openItems, setOpenItems] = React.useState<string[]>([]);
const hiddenItems = useDispatchMapState((state) => state.hiddenItems);
const { openCalls, setOpenCalls } = useDispatchMapState((state) => ({
setOpenCalls: state.setOpenCalls,
openCalls: state.openCalls,
}));

const map = useMap();
const t = useTranslations("Calls");
const { execute } = useFetch();
const { handleMarkerChange, handleMoveEnd } = useMarkerChange();

const hiddenItems = useDispatchMapState((state) => state.hiddenItems);
const { setCalls, calls } = useCall911State((state) => ({
setCalls: state.setCalls,
calls: state.calls,
}));
const calls = useCall911State((state) => state.calls);

const callsWithPosition = React.useMemo(() => {
return calls.filter((v) => v.gtaMapPosition || (v.position?.lat && v.position.lng));
}, [calls]);

function handleCallStateUpdate(callId: string, data: Full911Call) {
const prevIdx = calls.findIndex((v) => v.id === callId);
if (prevIdx !== -1) {
calls[prevIdx] = data;
}

setCalls(calls);
}

async function handleMoveEnd(e: LeafletEvent, call: Full911Call) {
const latLng = e.target._latlng;
const data = {
...call,
gtaMapPosition: null,
gtaMapPositionId: null,
position: { id: call.positionId ?? "", ...latLng },
};

handleCallStateUpdate(call.id, data);

const { json } = await execute<Put911CallByIdData>({
path: `/911-calls/${call.id}`,
method: "PUT",
data: {
gtaMapPosition: null,
gtaMapPositionId: null,
position: data.position,
},
});

handleCallStateUpdate(call.id, { ...data, ...json });
}

async function handleMarkerChange(call: Full911Call, type: "remove" | "set") {
const index = calls.findIndex((v) => v.id === call.id);
const coords = convertToMap(150 * index, 0, map);

const callData =
type === "set"
? { ...call, position: { ...coords, id: "null" } }
: { ...call, position: null };

handleCallStateUpdate(call.id, callData);

const { json } = await execute<Put911CallByIdData>({
path: `/911-calls/${call.id}`,
method: "PUT",
data: {
position: callData.position,
},
});

handleCallStateUpdate(call.id, { ...callData, ...json });
}

function handleToggle(callId: string) {
setOpenItems((p) => {
if (p.includes(callId)) {
return p.filter((v) => v !== callId);
}
const newOpenCalls = openCalls.includes(callId)
? openCalls.filter((v) => v !== callId)
: [...openCalls, callId];

return [...p, callId];
});
setOpenCalls(newOpenCalls);
}

return (
<>
{!hiddenItems[MapItem.CALLS] &&
callsWithPosition.map((call) => {
const callGtaPosition =
call.gtaMapPosition && call.gtaMapPositionId
? convertToMap(call.gtaMapPosition.x, call.gtaMapPosition.y, map)
: null;
const callPosition = call.position as { lat: number; lng: number };
const position = callGtaPosition ?? callPosition;

return (
<Marker
eventHandlers={{
moveend: (e) => handleMoveEnd(e, call),
}}
draggable
key={call.id}
position={position}
icon={CALL_ICON}
>
<Tooltip direction="top">{t("dragToMoveCallBlip")}</Tooltip>

<Popup minWidth={300}>
<p style={{ margin: 2, fontSize: 18 }}>
<strong>{t("location")}: </strong> {call.location}
</p>
<p style={{ margin: 2, fontSize: 18 }}>
<strong>{t("caller")}: </strong> {call.name}
</p>
<div style={{ display: "inline-block", margin: 2, fontSize: 18 }}>
<strong>{t("description")}: </strong> <CallDescription data={call} />
</div>

<div className="flex gap-2 mt-2">
<Button size="xs" className="!text-base" onPress={() => handleToggle(call.id)}>
{t("toggleCall")}
</Button>
<Button
size="xs"
variant="danger"
className="!text-base"
onPress={() => handleMarkerChange(call, "remove")}
>
{t("removeMarker")}
</Button>
</div>
</Popup>
</Marker>
);
})}

<ActiveMapCalls
openItems={openItems}
setOpenItems={setOpenItems}
hasMarker={(callId: string) => callsWithPosition.some((v) => v.id === callId)}
setMarker={handleMarkerChange}
/>
</>
!hiddenItems[MapItem.CALLS] &&
callsWithPosition.map((call) => {
const callGtaPosition =
call.gtaMapPosition && call.gtaMapPositionId
? convertToMap(call.gtaMapPosition.x, call.gtaMapPosition.y, map)
: null;
const callPosition = call.position as { lat: number; lng: number };
const position = callGtaPosition ?? callPosition;

return (
<Marker
eventHandlers={{
moveend: (e) => handleMoveEnd(e, call),
}}
draggable
key={call.id}
position={position}
icon={CALL_ICON}
>
<Tooltip direction="top">{t("dragToMoveCallBlip")}</Tooltip>

<Popup minWidth={300}>
<p style={{ margin: 2, fontSize: 18 }}>
<strong>{t("location")}: </strong> {call.location}
</p>
<p style={{ margin: 2, fontSize: 18 }}>
<strong>{t("caller")}: </strong> {call.name}
</p>
<div style={{ display: "inline-block", margin: 2, fontSize: 18 }}>
<strong>{t("description")}: </strong> <CallDescription data={call} />
</div>

<div className="flex gap-2 mt-2">
<Button size="xs" className="!text-base" onPress={() => handleToggle(call.id)}>
{t("toggleCall")}
</Button>
<Button
size="xs"
variant="danger"
className="!text-base"
onPress={() => handleMarkerChange(call, "remove")}
>
{t("removeMarker")}
</Button>
</div>
</Popup>
</Marker>
);
})
);
}
Loading

0 comments on commit 972d2fd

Please sign in to comment.