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
327 changes: 190 additions & 137 deletions src/app/map/[id]/components/Markers.tsx

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions src/app/map/[id]/components/PlacedMarkers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useMemo } from "react";
import { Layer, Source } from "react-map-gl/mapbox";

import { useFoldersQuery } from "@/app/map/[id]/hooks/useFolders";
import { useMapConfig } from "@/app/map/[id]/hooks/useMapConfig";
import { useMapViews } from "@/app/map/[id]/hooks/useMapViews";
import {
usePlacedMarkerState,
Expand All @@ -12,6 +13,7 @@ import type { FeatureCollection, Point } from "geojson";

export default function PlacedMarkers() {
const { viewConfig } = useMapViews();
const { mapConfig } = useMapConfig();
const { data: folders = [] } = useFoldersQuery();
const { data: placedMarkers = [] } = usePlacedMarkersQuery();
const { selectedPlacedMarkerId, getPlacedMarkerVisibility } =
Expand Down Expand Up @@ -40,6 +42,8 @@ export default function PlacedMarkers() {
properties: {
id: marker.id,
name: marker.label,
color:
mapConfig.placedMarkerColors?.[marker.id] ?? mapColors.markers.color,
},
geometry: {
type: "Point",
Expand All @@ -61,7 +65,7 @@ export default function PlacedMarkers() {
source="search-history"
paint={{
"circle-radius": ["interpolate", ["linear"], ["zoom"], 8, 3, 16, 8],
"circle-color": mapColors.markers.color,
"circle-color": ["get", "color"],
"circle-opacity": 0.8,
"circle-stroke-width": 1,
"circle-stroke-color": "#ffffff",
Expand All @@ -83,7 +87,7 @@ export default function PlacedMarkers() {
"text-anchor": "top",
}}
paint={{
"text-color": mapColors.markers.color,
"text-color": ["get", "color"],
"text-halo-color": "#ffffff",
"text-halo-width": 1,
}}
Expand All @@ -109,7 +113,7 @@ export default function PlacedMarkers() {
"circle-color": "#ffffff",
"circle-opacity": 0,
"circle-stroke-width": 2,
"circle-stroke-color": mapColors.markers.color,
"circle-stroke-color": ["get", "color"],
}}
/>
)}
Expand Down
6 changes: 6 additions & 0 deletions src/app/map/[id]/components/controls/ControlWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ export default function ControlWrapper({
name,
isVisible,
onVisibilityToggle,
color,
}: {
children: ReactNode;
name: string;
isVisible: boolean;
onVisibilityToggle: () => void;
layerType?: LayerType;
color?: string;
}) {
const getLayerColor = () => {
// Use custom color if provided, otherwise use default layer color
if (color) {
return color;
}
switch (layerType) {
case LayerType.Member:
return mapColors.member.color;
Expand Down
93 changes: 92 additions & 1 deletion src/app/map/[id]/components/controls/DataSourceItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { EyeIcon, EyeOffIcon, PencilIcon, TrashIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import ColorPalette from "@/components/ColorPalette";
import ContextMenuContentWithFocus from "@/components/ContextMenuContentWithFocus";
import DataSourceIcon from "@/components/DataSourceIcon";
import { MarkerDisplayMode } from "@/server/models/Map";
import { useTRPC } from "@/services/trpc/react";
import {
AlertDialog,
Expand All @@ -19,8 +21,13 @@ import {
} from "@/shadcn/ui/alert-dialog";
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/shadcn/ui/context-menu";
import { LayerType } from "@/types";
Expand Down Expand Up @@ -64,6 +71,31 @@ export default function DataSourceItem({

const isVisible = getDataSourceVisibility(dataSource?.id);

// Get current display mode (defaults to Clusters)
const currentDisplayMode =
mapConfig.markerDisplayModes?.[dataSource.id] ?? MarkerDisplayMode.Clusters;

// Get current color (defaults to layer color)
const currentColor = mapConfig.markerColors?.[dataSource.id] ?? layerColor;

const handleDisplayModeChange = (mode: MarkerDisplayMode) => {
updateMapConfig({
markerDisplayModes: {
...mapConfig.markerDisplayModes,
[dataSource.id]: mode,
},
});
};

const handleColorChange = (color: string) => {
updateMapConfig({
markerColors: {
...mapConfig.markerColors,
[dataSource.id]: color,
},
});
};

// Focus management for rename input
useEffect(() => {
if (isRenaming) {
Expand Down Expand Up @@ -138,12 +170,13 @@ export default function DataSourceItem({
onVisibilityToggle={() =>
setDataSourceVisibility(dataSource?.id, !isVisible)
}
color={currentColor}
>
<ContextMenu>
<ContextMenuTrigger asChild>
<button
className="flex w-full items-center justify-between gap-2 min-h-full cursor-pointer hover:bg-neutral-100 border-2 rounded"
style={{ borderColor: isSelected ? layerColor : "transparent" }}
style={{ borderColor: isSelected ? currentColor : "transparent" }}
onClick={() =>
!isRenaming && handleDataSourceSelect(dataSource.id)
}
Expand Down Expand Up @@ -216,6 +249,64 @@ export default function DataSourceItem({
)}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger>
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded border border-neutral-300"
style={{ backgroundColor: currentColor }}
/>
<span>Color</span>
</div>
</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-auto p-2">
<ColorPalette
selectedColor={currentColor}
onColorSelect={handleColorChange}
/>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuLabel>Display as</ContextMenuLabel>
<ContextMenuCheckboxItem
checked={currentDisplayMode === MarkerDisplayMode.Clusters}
onCheckedChange={(checked) => {
if (checked) {
handleDisplayModeChange(MarkerDisplayMode.Clusters);
}
}}
>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 rounded-full border border-neutral-300 flex items-center justify-center flex-shrink-0"
style={{ backgroundColor: currentColor }}
>
<span className="text-[8px] font-semibold text-white leading-none">
5
</span>
</div>
<span>Cluster</span>
</div>
</ContextMenuCheckboxItem>
<ContextMenuCheckboxItem
checked={currentDisplayMode === MarkerDisplayMode.Heatmap}
onCheckedChange={(checked) => {
if (checked) {
handleDisplayModeChange(MarkerDisplayMode.Heatmap);
}
}}
>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 rounded border border-neutral-300 flex-shrink-0 relative overflow-hidden"
style={{
background: `radial-gradient(circle, ${currentColor} 0%, ${currentColor}80 30%, ${currentColor}40 60%, ${currentColor}20 100%)`,
}}
/>
<span>Heatmap</span>
</div>
</ContextMenuCheckboxItem>
<ContextMenuSeparator />
<ContextMenuItem
variant="destructive"
onClick={() => setShowRemoveDialog(true)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@ import type { PlacedMarker } from "@/server/models/PlacedMarker";

export default function MarkerDragOverlay({
marker,
color,
}: {
marker: PlacedMarker;
color?: string;
}) {
const markerColor = color || mapColors.markers.color;

return (
<div className="flex items-center gap-2 p-0.5 bg-white border border-blue-300 rounded shadow-lg pointer-events-none">
<div className="relative flex items-center gap-1 p-0.5 bg-white border border-blue-300 rounded shadow-lg pointer-events-none">
<div
className="w-2 h-2 rounded-full aspect-square flex-shrink-0"
style={{ backgroundColor: mapColors.markers.color }}
className="absolute top-0 left-0 h-full w-1 shrink-0 rounded-xs"
style={{ background: markerColor }}
/>
<span className="text-sm leading-relaxed flex-1 break-all">
{marker.label}
</span>
<div className="grow pl-3">
<span className="text-sm leading-relaxed flex-1 break-all">
{marker.label}
</span>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ import {
} from "@/app/map/[id]/utils/position";
import { useTRPC } from "@/services/trpc/react";
import { LayerType } from "@/types";
import { useMapConfig } from "../../../hooks/useMapConfig";
import { useMapId } from "../../../hooks/useMapCore";
import { mapColors } from "../../../styles";
import DataSourceControl from "../DataSourceItem";
import EmptyLayer from "../LayerEmptyMessage";
import MarkerDragOverlay from "./MarkerDragOverlay";
Expand All @@ -63,6 +65,7 @@ export default function MarkersList({
const trpc = useTRPC();
const queryClient = useQueryClient();
const { viewConfig } = useMapViews();
const { mapConfig, updateMapConfig } = useMapConfig();
const { data: folders = [] } = useFoldersQuery();
const { updateFolder } = useFolderMutations();
const { data: placedMarkers = [] } = usePlacedMarkersQuery();
Expand Down Expand Up @@ -277,6 +280,16 @@ export default function MarkersList({
position: newPosition,
});

// Update marker color to match folder color
const folderColor =
mapConfig.folderColors?.[folderId] || mapColors.markers.color;
updateMapConfig({
placedMarkerColors: {
...mapConfig.placedMarkerColors,
[activeMarker.id]: folderColor,
},
});

// Animate movement - pulse the folder that received the marker
setPulsingFolderId(folderId);
} else if (over && over.id === "unassigned") {
Expand All @@ -289,6 +302,8 @@ export default function MarkersList({
folderId: null,
position: newPosition,
});
// Keep the marker's current color when moved to unassigned
// (don't reset it, just keep what it has)
} else if (over && over.id.toString().startsWith("marker-")) {
// Handle reordering within the same container OR moving to a different container
const overMarkerId = over.id.toString().replace("marker-", "");
Expand Down Expand Up @@ -325,10 +340,29 @@ export default function MarkersList({
folderId: overMarker.folderId, // Move to the same folder as the marker we're dropping on
position: newPosition,
});

// Update marker color to match the folder it's being moved to
if (overMarker.folderId) {
const folderColor =
mapConfig.folderColors?.[overMarker.folderId] ||
mapColors.markers.color;
updateMapConfig({
placedMarkerColors: {
...mapConfig.placedMarkerColors,
[activeMarker.id]: folderColor,
},
});
}
}
}
},
[placedMarkers, updatePlacedMarker, setPulsingFolderId],
[
placedMarkers,
updatePlacedMarker,
setPulsingFolderId,
mapConfig,
updateMapConfig,
],
);

const handleDragEndFolder = useCallback(
Expand Down Expand Up @@ -397,13 +431,27 @@ export default function MarkersList({
return sortByPositionAndId(folders);
}, [folders]);

// Get active marker for drag overlay
// Get active marker and color for drag overlay
const getActiveMarker = () => {
if (!activeId) return null;
const markerId = activeId.replace("marker-", "");
return placedMarkers.find((marker) => marker.id === markerId) || null;
};

const getActiveMarkerColor = () => {
const marker = getActiveMarker();
if (!marker) return mapColors.markers.color;

// Get marker color (check folder color first, then marker color, then default)
if (marker.folderId && mapConfig.folderColors?.[marker.folderId]) {
return mapConfig.folderColors[marker.folderId];
}
if (mapConfig.placedMarkerColors?.[marker.id]) {
return mapConfig.placedMarkerColors[marker.id];
}
return mapColors.markers.color;
};

const hasMarkers =
membersDataSource ||
markerDataSources?.length ||
Expand Down Expand Up @@ -493,7 +541,10 @@ export default function MarkersList({
{createPortal(
<DragOverlay dropAnimation={null}>
{activeId && getActiveMarker() && (
<MarkerDragOverlay marker={getActiveMarker() as PlacedMarker} />
<MarkerDragOverlay
marker={getActiveMarker() as PlacedMarker}
color={getActiveMarkerColor()}
/>
)}
</DragOverlay>,
document.body,
Expand Down
Loading