Skip to content
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

Mobile UI Enhancements #128

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion app/src/app/components/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const MapComponent: React.FC = () => {

return (
<div
className={`h-full w-full-minus-sidebar relative
className={`h-full relative w-full flex-1 lg:h-screen landscape:h-screen
${mapLock ? "pointer-events-none" : ""}
`}
ref={mapContainer}
Expand Down
2 changes: 1 addition & 1 deletion app/src/app/components/sidebar/ColorPicker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function ColorPicker() {
<div>
<RadioGroupRoot
onValueChange={handleRadioChange}
defaultValue={colorArray[0]}
defaultValue={selectedZone}
>
{colorArray.map((color, i) => (
<RadioGroupItem
Expand Down
66 changes: 66 additions & 0 deletions app/src/app/components/sidebar/DataPanels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Box, Flex, Heading } from "@radix-ui/themes";
import { MapModeSelector } from "./MapModeSelector";
import { ColorPicker } from "./ColorPicker";
import { ResetMapButton } from "./ResetMapButton";
import { GerryDBViewSelector } from "./GerryDBViewSelector";
import { HorizontalBar } from "./charts/HorizontalBarChart";
import { useMapStore } from "@/app/store/mapStore";
import { Tabs, Text } from "@radix-ui/themes";
import Layers from "./Layers";
import React from "react";

interface DataPanelSpec {
title: string;
label: string;
icon?: React.ReactNode;
content?: React.ReactNode;
}

interface DataPanelsProps {
defaultPanel?: string;
panels?: DataPanelSpec[];
}

const defaultPanels: DataPanelSpec[] = [
{
title: "population",
label: "Population",
content: <HorizontalBar />,
},
{
title: "layers",
label: "Data layers",
content: <Layers />,
},
{
title: "evaluation",
label: "Evaluation",
content: <Text size="2"> Unimplemented </Text>,
},
];

const DataPanels: React.FC<DataPanelsProps> = ({
defaultPanel = defaultPanels[0].title,
panels = defaultPanels,
}) => {
return (
<Tabs.Root defaultValue={defaultPanel}>
<Tabs.List>
{panels.map((panel) => (
<Tabs.Trigger key={panel.title} value={panel.title}>
{panel.label}
</Tabs.Trigger>
))}
</Tabs.List>
<Box pt="3">
{panels.map((panel) => (
<Tabs.Content key={panel.title} value={panel.title}>
{panel.content}
</Tabs.Content>
))}
</Box>
</Tabs.Root>
);
};

export default DataPanels;
2 changes: 1 addition & 1 deletion app/src/app/components/sidebar/MapModeSelector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function MapModeSelector() {
defaultValue="default"
value={activeTool}
onValueChange={handleRadioChange}
columns={{ initial: "1", sm: "3" }}
columns={{ initial: "3" }}
>
{activeTools.map((tool) => (
<Flex key={`${tool.mode}-flex`}>
Expand Down
32 changes: 32 additions & 0 deletions app/src/app/components/sidebar/MobileColorPicker.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { useState } from "react";
import { Button, IconButton, Popover } from "@radix-ui/themes"; // Import Popover from Radix
import { ColorPicker } from "./ColorPicker";
import { colorScheme } from "@/app/constants/colors";
import { useMapStore } from "@/app/store/mapStore";
import { ColorWheelIcon } from "@radix-ui/react-icons";

export const MobileColorPicker = () => {
const [open, setOpen] = useState(false);
const selectedZone = useMapStore((state) => state.selectedZone);

const zoneIndex = selectedZone ? selectedZone - 1 : 0;
const color = colorScheme[zoneIndex];

return (
<Popover.Root open={open} onOpenChange={setOpen}>
<Popover.Trigger>
<IconButton
radius="full"
style={{ background: color }}
size="3"
aria-label="Choose map districtr assignment brush color"
>
<ColorWheelIcon />
</IconButton>
</Popover.Trigger>
<Popover.Content width="95vw">
<ColorPicker />
</Popover.Content>
</Popover.Root>
);
};
76 changes: 76 additions & 0 deletions app/src/app/components/sidebar/MobileTopNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Box, Button, Flex, Heading } from "@radix-ui/themes";
import React, { useRef, useState } from "react";
import { Cross2Icon, GearIcon } from "@radix-ui/react-icons";
import DataPanels from "./DataPanels";

const MobileTopNav = () => {
const [dataPanelOpen, setDataPanelOpen] = useState(false);
const handleToggleDataPanel = () => setDataPanelOpen((prev) => !prev);

const boxRef = useRef<HTMLDivElement>(null);
const topBarHeight =
boxRef.current?.getClientRects()?.[0]?.height || 44.90625;
const isLandscape = typeof window !== 'undefined' && window.matchMedia("(orientation: landscape)").matches;

return (
<Box
className="w-full bg-white z-10 shadow-md flex-none relative landscape:w-0"
ref={boxRef}
display={{
initial: "block",
md: "none",
}}
>
<Flex direction="row" gap="1" align="center" justify={"between"} pr="3">
<Heading
as="h3"
size="3"
className="border-r-2 p-3 flex-none landscape:hidden"
>
Districtr
</Heading>
<Flex
align="center"
className="landscape:z-50 landscape:bg-white landscape:absolute landscape:top-1 landscape:left-1 landscape:w-auto"
>
<Button
onClick={handleToggleDataPanel}
variant="outline"
color={dataPanelOpen ? "indigo" : "gray"}
className="landscape:bg-white"
style={{
background: "white",
}}
>
<GearIcon fill={dataPanelOpen ? "indigo" : "gray"} />
Reports & Settings
</Button>
<Flex
className={`flex-none overflow-hidden transition-all duration-300 ease-in-out ${
dataPanelOpen ? "w-4 ml-2" : "w-0 ml-0"
}
p-0 m-0

`}
>
<Button onClick={handleToggleDataPanel} variant="ghost" color="red">
<Cross2Icon />
</Button>
</Flex>
</Flex>
</Flex>
{dataPanelOpen && (
<Box
className={`absolute p-4 z-20 bg-white border-t-2 w-full top-[100%] overflow-y-auto landscape:w-[100vw] landscape:top-0 landscape:pt-12 landscape:h-[100vh]`}
style={{
height: isLandscape ? undefined : `calc(100vh - ${topBarHeight}px)`,
}}
>
<DataPanels />
</Box>
)}
</Box>
);
};

export default MobileTopNav;
65 changes: 36 additions & 29 deletions app/src/app/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,66 @@
import { Box, Flex, Heading } from "@radix-ui/themes";
import { MapModeSelector } from "./MapModeSelector";
import { ColorPicker } from "./ColorPicker";
import { MobileColorPicker } from "./MobileColorPicker"
import { ResetMapButton } from "./ResetMapButton";
import { GerryDBViewSelector } from "./GerryDBViewSelector";
import { HorizontalBar } from "./charts/HorizontalBarChart";
import { useMapStore } from "@/app/store/mapStore";
import { Tabs, Text } from "@radix-ui/themes";
import Layers from "./Layers";
import PaintByCounty from "./PaintByCounty";
import { BrushSizeSelector } from "./BrushSizeSelector";
import React from "react";
import DataPanels from "./DataPanels";

export default function SidebarComponent() {
const activeTool = useMapStore((state) => state.activeTool);

return (
<Box
p="3"
className="max-w-sidebar w-sidebar z-10 shadow-md h-screen overflow-y-auto"
className="w-full z-10 shadow-md flex-none overflow-y-auto
border-t lg:border-t-0
lg:h-screen lg:max-w-sidebar lg:w-sidebar
landscape:border-t-0
landscape:h-screen landscape:max-w-[40vw] landscape:w-[40vw]

"
>
<Flex direction="column" gap="3">
<Heading as="h3" size="3">
<Heading as="h3" size="3" className="hidden lg:block">
Districtr
</Heading>
<GerryDBViewSelector />
<MapModeSelector />
{activeTool === "brush" || activeTool === "eraser" ? (
<div>
<div
className="gap-4 lg:gap-0 landscape:gap-0
flex flex-row-reverse lg:flex-col landscape:flex-col
justify-around
">
<div className="flex-grow">
<BrushSizeSelector />
<PaintByCounty />{" "}
</div>
) : null}
{activeTool === "brush" ? (
<div>
<ColorPicker />
</div>
{activeTool === "brush" ? (
<div className="flex-grow-0">
<span className="hidden md:block landscape:block">
<ColorPicker />
</span>
<span className="md:hidden landscape:hidden">
<MobileColorPicker />
</span>
</div>
) : null}
</div>
) : null}
<ResetMapButton />
<Tabs.Root defaultValue="layers">
<Tabs.List>
<Tabs.Trigger value="population"> Population </Tabs.Trigger>
<Tabs.Trigger value="layers"> Data layers </Tabs.Trigger>
<Tabs.Trigger value="evaluation"> Evaluation </Tabs.Trigger>
</Tabs.List>
<Box pt="3">
<Tabs.Content value="population">
<HorizontalBar />
</Tabs.Content>
<Tabs.Content value="layers">
<Layers />
</Tabs.Content>
<Tabs.Content value="evaluation">
<Text size="2"> Unimplemented </Text>
</Tabs.Content>
</Box>
</Tabs.Root>
<Box
display={{
initial: "none",
md: "inline",
}}
>
<DataPanels defaultPanel="layers" />
</Box>
</Flex>
</Box>
);
Expand Down
6 changes: 4 additions & 2 deletions app/src/app/map/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from "react";
import { MapContextMenu } from "../components/ContextMenu";
import { MapComponent } from "../components/Map";
import SidebarComponent from "../components/sidebar/Sidebar";
import MobileTopNav from "../components/sidebar/MobileTopNav";
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "../utils/api/queryClient";

Expand All @@ -11,9 +12,10 @@ export default function Map() {
if (queryClient) {
return (
<QueryClientProvider client={queryClient}>
<div className="h-screen w-screen flex justify-between p">
<MapComponent />
<div className="h-screen w-screen flex justify-between p flex-col-reverse lg:flex-row-reverse landscape:flex-row-reverse">
<SidebarComponent />
<MapComponent />
<MobileTopNav />
<MapContextMenu />
</div>
</QueryClientProvider>
Expand Down
8 changes: 6 additions & 2 deletions app/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ import { queryClient } from "./utils/api/queryClient";
export default function Home() {
return (
<QueryClientProvider client={queryClient}>
<main>
<div className="h-screen w-screen flex justify-between p">
<main style={{background:"red"}} id="test">
<div className="h-screen w-screen flex justify-between p"
style={{
flexDirection:"column",
background: "red"
}}>
<MapComponent />
<SidebarComponent />
</div>
Expand Down
10 changes: 9 additions & 1 deletion app/src/app/utils/events/mapEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,15 @@ export const handleMapMouseMove = (
mapStore.brushSize,
paintLayers,
);
// sourceCapabilities exists on the UIEvent constructor, which does not appear
// properly tpyed in the default map events
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/sourceCapabilities
const isTouchEvent = 'touches' in e || (e.originalEvent as any)?.sourceCapabilities?.firesTouchEvents

const isBrushingTool =
sourceLayer && ["brush", "eraser"].includes(activeTool);
if (isBrushingTool) {

if (isBrushingTool && !isTouchEvent) {
setHoverFeatures(selectedFeatures);
}

Expand Down Expand Up @@ -222,6 +228,8 @@ export const mapEvents = [
{ action: "mouseover", handler: handleMapMouseOver },
{ action: "mouseleave", handler: handleMapMouseLeave },
{ action: "touchleave", handler: handleMapMouseLeave },
{ action: "touchend", handler: handleMapMouseUp },
{ action: "touchcancel", handler: handleMapMouseUp },
{ action: "mouseout", handler: handleMapMouseOut },
{ action: "mousemove", handler: handleMapMouseMove },
{ action: "touchmove", handler: handleMapMouseMove },
Expand Down