From b8476c66d28b82220db90357d33e666db52eedd4 Mon Sep 17 00:00:00 2001 From: Arbyhisenaj <41119392+Arbyhisenaj@users.noreply.github.com> Date: Wed, 21 Jan 2026 11:12:07 +0000 Subject: [PATCH 1/8] clustering --- src/app/map/[id]/components/Markers.tsx | 330 +++++++++++------- .../components/controls/DataSourceItem.tsx | 43 ++- src/server/models/Map.ts | 6 + 3 files changed, 248 insertions(+), 131 deletions(-) diff --git a/src/app/map/[id]/components/Markers.tsx b/src/app/map/[id]/components/Markers.tsx index 833fbe80..07e7b7b1 100644 --- a/src/app/map/[id]/components/Markers.tsx +++ b/src/app/map/[id]/components/Markers.tsx @@ -5,6 +5,7 @@ import { useMapConfig } from "@/app/map/[id]/hooks/useMapConfig"; import { useMapViews } from "@/app/map/[id]/hooks/useMapViews"; import { useMarkerQueries } from "@/app/map/[id]/hooks/useMarkerQueries"; import { publicMapColorSchemes } from "@/app/map/[id]/styles"; +import { MarkerDisplayMode } from "@/server/models/Map"; import { useLayers } from "../hooks/useLayers"; import { mapColors } from "../styles"; import { PublicFiltersContext } from "../view/[viewIdOrHost]/publish/context/PublicFiltersContext"; @@ -14,19 +15,19 @@ import type { FeatureCollection } from "geojson"; const MARKER_CLIENT_EXCLUDED_KEY = "__clientExcluded"; -// function hexToRgb(hex: string) { -// const normalized = hex.replace("#", ""); -// const bigint = parseInt(normalized, 16); -// const r = (bigint >> 16) & 255; -// const g = (bigint >> 8) & 255; -// const b = bigint & 255; -// return { r, g, b }; -// } +function hexToRgb(hex: string) { + const normalized = hex.replace("#", ""); + const bigint = parseInt(normalized, 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + return { r, g, b }; +} -// function rgbaString(hex: string, alpha: number) { -// const { r, g, b } = hexToRgb(hex); -// return `rgba(${r}, ${g}, ${b}, ${alpha})`; -// } +function rgbaString(hex: string, alpha: number) { + const { r, g, b } = hexToRgb(hex); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} export default function Markers() { const { viewConfig } = useMapViews(); @@ -57,6 +58,7 @@ export default function Markers() { key={memberMarkers.dataSourceId} dataSourceMarkers={memberMarkers} isMembers + mapConfig={mapConfig} /> )} {otherMarkers.map((markers) => { @@ -72,6 +74,7 @@ export default function Markers() { key={markers.dataSourceId} dataSourceMarkers={markers} isMembers={false} + mapConfig={mapConfig} /> ); })} @@ -82,13 +85,20 @@ export default function Markers() { function DataSourceMarkers({ dataSourceMarkers, isMembers, + mapConfig, }: { dataSourceMarkers: { dataSourceId: string; markers: MarkerFeature[] }; isMembers: boolean; + mapConfig: { markerDisplayModes?: Record }; }) { const { filteredRecords, publicFilters } = useContext(PublicFiltersContext); const { publicMap, colorScheme } = useContext(PublicMapContext); + // Get display mode for this data source (defaults to Clusters) + const displayMode = + mapConfig.markerDisplayModes?.[dataSourceMarkers.dataSourceId] ?? + MarkerDisplayMode.Clusters; + const safeMarkers = useMemo(() => { // Don't add MARKER_CLIENT_EXCLUDED_KEY property if no public filters exist if (Object.keys(publicFilters).length === 0) { @@ -135,7 +145,7 @@ function DataSourceMarkers({ key={sourceId} type="geojson" data={safeMarkers} - cluster={true} + cluster={displayMode === MarkerDisplayMode.Clusters} clusterMaxZoom={publicMap ? 22 : 11} clusterRadius={50} clusterProperties={{ @@ -154,78 +164,69 @@ function DataSourceMarkers({ ], }} > - - - {/* TODO: Restore this with a switch + {displayMode === MarkerDisplayMode.Clusters && ( + + )} + {displayMode === MarkerDisplayMode.Clusters && ( + + )} + {displayMode === MarkerDisplayMode.Heatmap && ( */} - - ", ["length", ["get", "name"]], 20], "...", ""], - ], - "text-font": ["DIN Pro Medium", "Arial Unicode MS Bold"], - "text-size": 12, - "text-transform": "uppercase", - "text-offset": [0, -1.25], - }} - paint={{ - "text-color": color, - "text-halo-color": "#ffffff", - "text-halo-width": 1, - }} - /> + /> + )} + {/* Individual pins - only show for cluster mode or at high zoom in heatmap mode */} + {displayMode === MarkerDisplayMode.Clusters && ( + + )} + {displayMode === MarkerDisplayMode.Clusters && ( + ", ["length", ["get", "name"]], 20], + "...", + "", + ], + ], + "text-font": ["DIN Pro Medium", "Arial Unicode MS Bold"], + "text-size": 12, + "text-transform": "uppercase", + "text-offset": [0, -1.25], + }} + paint={{ + "text-color": color, + "text-halo-color": "#ffffff", + "text-halo-width": 1, + }} + /> + )} + {displayMode === MarkerDisplayMode.Heatmap && ( + + )} + {displayMode === MarkerDisplayMode.Heatmap && ( + ", ["length", ["get", "name"]], 20], + "...", + "", + ], + ], + "text-font": ["DIN Pro Medium", "Arial Unicode MS Bold"], + "text-size": 12, + "text-transform": "uppercase", + "text-offset": [0, -1.25], + }} + paint={{ + "text-color": color, + "text-halo-color": "#ffffff", + "text-halo-width": 1, + }} + /> + )} ); } diff --git a/src/app/map/[id]/components/controls/DataSourceItem.tsx b/src/app/map/[id]/components/controls/DataSourceItem.tsx index 01f5c021..5227f688 100644 --- a/src/app/map/[id]/components/controls/DataSourceItem.tsx +++ b/src/app/map/[id]/components/controls/DataSourceItem.tsx @@ -2,10 +2,11 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { EyeIcon, EyeOffIcon, PencilIcon, TrashIcon } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import ContextMenuContentWithFocus from "@/components/ContextMenuContentWithFocus"; import DataSourceIcon from "@/components/DataSourceIcon"; +import { MarkerDisplayMode } from "@/server/models/Map"; import { useTRPC } from "@/services/trpc/react"; import { AlertDialog, @@ -19,7 +20,9 @@ import { } from "@/shadcn/ui/alert-dialog"; import { ContextMenu, + ContextMenuCheckboxItem, ContextMenuItem, + ContextMenuLabel, ContextMenuSeparator, ContextMenuTrigger, } from "@/shadcn/ui/context-menu"; @@ -64,6 +67,22 @@ export default function DataSourceItem({ const isVisible = getDataSourceVisibility(dataSource?.id); + // Get current display mode (defaults to Clusters) + const currentDisplayMode = useMemo(() => { + return ( + mapConfig.markerDisplayModes?.[dataSource.id] ?? MarkerDisplayMode.Clusters + ); + }, [mapConfig.markerDisplayModes, dataSource.id]); + + const handleDisplayModeChange = (mode: MarkerDisplayMode) => { + updateMapConfig({ + markerDisplayModes: { + ...mapConfig.markerDisplayModes, + [dataSource.id]: mode, + }, + }); + }; + // Focus management for rename input useEffect(() => { if (isRenaming) { @@ -216,6 +235,28 @@ export default function DataSourceItem({ )} + Display as + { + if (checked) { + handleDisplayModeChange(MarkerDisplayMode.Clusters); + } + }} + > + Cluster + + { + if (checked) { + handleDisplayModeChange(MarkerDisplayMode.Heatmap); + } + }} + > + Heatmap + + setShowRemoveDialog(true)} diff --git a/src/server/models/Map.ts b/src/server/models/Map.ts index 713eedc6..e79991fb 100644 --- a/src/server/models/Map.ts +++ b/src/server/models/Map.ts @@ -1,9 +1,15 @@ import z from "zod"; import type { ColumnType, Generated, Insertable, Updateable } from "kysely"; +export enum MarkerDisplayMode { + Clusters = "clusters", + Heatmap = "heatmap", +} + export const mapConfigSchema = z.object({ markerDataSourceIds: z.array(z.string()), membersDataSourceId: z.string().nullish(), + markerDisplayModes: z.record(z.nativeEnum(MarkerDisplayMode)).optional(), }); export type MapConfig = z.infer; From 5369fc499cb08b36f80bdc29b94547ba3ef0ff16 Mon Sep 17 00:00:00 2001 From: Arbyhisenaj <41119392+Arbyhisenaj@users.noreply.github.com> Date: Wed, 21 Jan 2026 11:13:57 +0000 Subject: [PATCH 2/8] removed use memo --- src/app/map/[id]/components/controls/DataSourceItem.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/app/map/[id]/components/controls/DataSourceItem.tsx b/src/app/map/[id]/components/controls/DataSourceItem.tsx index 5227f688..dd472690 100644 --- a/src/app/map/[id]/components/controls/DataSourceItem.tsx +++ b/src/app/map/[id]/components/controls/DataSourceItem.tsx @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { EyeIcon, EyeOffIcon, PencilIcon, TrashIcon } from "lucide-react"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import ContextMenuContentWithFocus from "@/components/ContextMenuContentWithFocus"; import DataSourceIcon from "@/components/DataSourceIcon"; @@ -68,11 +68,8 @@ export default function DataSourceItem({ const isVisible = getDataSourceVisibility(dataSource?.id); // Get current display mode (defaults to Clusters) - const currentDisplayMode = useMemo(() => { - return ( - mapConfig.markerDisplayModes?.[dataSource.id] ?? MarkerDisplayMode.Clusters - ); - }, [mapConfig.markerDisplayModes, dataSource.id]); + const currentDisplayMode = + mapConfig.markerDisplayModes?.[dataSource.id] ?? MarkerDisplayMode.Clusters; const handleDisplayModeChange = (mode: MarkerDisplayMode) => { updateMapConfig({ From 14fc5176389690190491e859d072a4f3d02edc7e Mon Sep 17 00:00:00 2001 From: Arbyhisenaj <41119392+Arbyhisenaj@users.noreply.github.com> Date: Wed, 21 Jan 2026 11:24:30 +0000 Subject: [PATCH 3/8] colours pre-back end --- src/app/map/[id]/components/Markers.tsx | 18 ++++-- .../components/controls/ControlWrapper.tsx | 6 ++ .../components/controls/DataSourceItem.tsx | 60 +++++++++++++++++- src/components/ColorPalette.tsx | 63 +++++++++++++++++++ src/server/models/Map.ts | 1 + 5 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 src/components/ColorPalette.tsx diff --git a/src/app/map/[id]/components/Markers.tsx b/src/app/map/[id]/components/Markers.tsx index 07e7b7b1..08c7eb18 100644 --- a/src/app/map/[id]/components/Markers.tsx +++ b/src/app/map/[id]/components/Markers.tsx @@ -89,7 +89,10 @@ function DataSourceMarkers({ }: { dataSourceMarkers: { dataSourceId: string; markers: MarkerFeature[] }; isMembers: boolean; - mapConfig: { markerDisplayModes?: Record }; + mapConfig: { + markerDisplayModes?: Record; + markerColors?: Record; + }; }) { const { filteredRecords, publicFilters } = useContext(PublicFiltersContext); const { publicMap, colorScheme } = useContext(PublicMapContext); @@ -133,11 +136,14 @@ function DataSourceMarkers({ publicMap?.id && colorScheme ? publicMapColorSchemes[colorScheme]?.primary : ""; - const color = publicMapColor - ? publicMapColor - : isMembers - ? mapColors.member.color - : mapColors.dataSource.color; + + // Get custom color from mapConfig, or fall back to defaults + const customColor = mapConfig.markerColors?.[dataSourceMarkers.dataSourceId]; + const defaultColor = isMembers + ? mapColors.member.color + : mapColors.dataSource.color; + + const color = publicMapColor || customColor || defaultColor; return ( 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; diff --git a/src/app/map/[id]/components/controls/DataSourceItem.tsx b/src/app/map/[id]/components/controls/DataSourceItem.tsx index dd472690..10866acf 100644 --- a/src/app/map/[id]/components/controls/DataSourceItem.tsx +++ b/src/app/map/[id]/components/controls/DataSourceItem.tsx @@ -4,6 +4,7 @@ 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"; @@ -24,6 +25,9 @@ import { ContextMenuItem, ContextMenuLabel, ContextMenuSeparator, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, ContextMenuTrigger, } from "@/shadcn/ui/context-menu"; import { LayerType } from "@/types"; @@ -71,6 +75,10 @@ export default function DataSourceItem({ 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: { @@ -80,6 +88,15 @@ export default function DataSourceItem({ }); }; + const handleColorChange = (color: string) => { + updateMapConfig({ + markerColors: { + ...mapConfig.markerColors, + [dataSource.id]: color, + }, + }); + }; + // Focus management for rename input useEffect(() => { if (isRenaming) { @@ -154,12 +171,13 @@ export default function DataSourceItem({ onVisibilityToggle={() => setDataSourceVisibility(dataSource?.id, !isVisible) } + color={currentColor} > + ))} + + ); +} diff --git a/src/server/models/Map.ts b/src/server/models/Map.ts index e79991fb..d73e8f63 100644 --- a/src/server/models/Map.ts +++ b/src/server/models/Map.ts @@ -10,6 +10,7 @@ export const mapConfigSchema = z.object({ markerDataSourceIds: z.array(z.string()), membersDataSourceId: z.string().nullish(), markerDisplayModes: z.record(z.nativeEnum(MarkerDisplayMode)).optional(), + markerColors: z.record(z.string()).optional(), }); export type MapConfig = z.infer; From 871458813a6fa24b727d09ee77a6fd989a92c44e Mon Sep 17 00:00:00 2001 From: Arbyhisenaj <41119392+Arbyhisenaj@users.noreply.github.com> Date: Wed, 21 Jan 2026 11:26:21 +0000 Subject: [PATCH 4/8] colour preservation using config --- src/server/trpc/routers/map.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/trpc/routers/map.ts b/src/server/trpc/routers/map.ts index 1f1480a6..74b08b01 100644 --- a/src/server/trpc/routers/map.ts +++ b/src/server/trpc/routers/map.ts @@ -125,6 +125,8 @@ export const mapRouter = router({ const config = { markerDataSourceIds: mapConfig.markerDataSourceIds?.filter(Boolean), membersDataSourceId: mapConfig.membersDataSourceId, + markerDisplayModes: mapConfig.markerDisplayModes, + markerColors: mapConfig.markerColors, } as z.infer; return updateMap(mapId, { config }); From 546f161337de53f3c557e6891149722ab075ed1f Mon Sep 17 00:00:00 2001 From: Arbyhisenaj <41119392+Arbyhisenaj@users.noreply.github.com> Date: Wed, 21 Jan 2026 11:27:59 +0000 Subject: [PATCH 5/8] individual marker colours --- src/app/map/[id]/components/PlacedMarkers.tsx | 10 +++-- .../MarkersControl/SortableMarkerItem.tsx | 40 +++++++++++++++++++ src/server/models/Map.ts | 1 + src/server/trpc/routers/map.ts | 1 + 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/app/map/[id]/components/PlacedMarkers.tsx b/src/app/map/[id]/components/PlacedMarkers.tsx index faa95c04..5438ecea 100644 --- a/src/app/map/[id]/components/PlacedMarkers.tsx +++ b/src/app/map/[id]/components/PlacedMarkers.tsx @@ -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, @@ -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 } = @@ -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", @@ -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", @@ -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, }} @@ -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"], }} /> )} diff --git a/src/app/map/[id]/components/controls/MarkersControl/SortableMarkerItem.tsx b/src/app/map/[id]/components/controls/MarkersControl/SortableMarkerItem.tsx index 44bfbc7b..e901f28a 100644 --- a/src/app/map/[id]/components/controls/MarkersControl/SortableMarkerItem.tsx +++ b/src/app/map/[id]/components/controls/MarkersControl/SortableMarkerItem.tsx @@ -5,15 +5,22 @@ import { CSS } from "@dnd-kit/utilities"; import { EyeIcon, EyeOffIcon, PencilIcon, TrashIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; +import ColorPalette from "@/components/ColorPalette"; import DeleteConfirmationDialog from "@/components/DeleteConfirmationDialog"; +import { mapColors } from "../../../styles"; import { ContextMenu, ContextMenuContent, ContextMenuItem, + ContextMenuLabel, ContextMenuSeparator, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, ContextMenuTrigger, } from "@/shadcn/ui/context-menu"; import { LayerType } from "@/types"; +import { useMapConfig } from "../../../hooks/useMapConfig"; import { useMapRef } from "../../../hooks/useMapCore"; import { usePlacedMarkerMutations, @@ -47,11 +54,25 @@ export default function SortableMarkerItem({ setPlacedMarkerVisibility, } = usePlacedMarkerState(); const { updatePlacedMarker, deletePlacedMarker } = usePlacedMarkerMutations(); + const { mapConfig, updateMapConfig } = useMapConfig(); const mapRef = useMapRef(); const [isEditing, setEditing] = useState(false); const [editText, setEditText] = useState(marker.label); const [showDeleteDialog, setShowDeleteDialog] = useState(false); + // Get current color (defaults to marker color) + const currentColor = + mapConfig.placedMarkerColors?.[marker.id] ?? mapColors.markers.color; + + const handleColorChange = (color: string) => { + updateMapConfig({ + placedMarkerColors: { + ...mapConfig.placedMarkerColors, + [marker.id]: color, + }, + }); + }; + // Check if this marker is the one being dragged (even outside its container) const isCurrentlyDragging = isDragging || activeId === `marker-${marker.id}`; const isVisible = getPlacedMarkerVisibility(marker.id); @@ -121,6 +142,7 @@ export default function SortableMarkerItem({ onVisibilityToggle={() => setPlacedMarkerVisibility(marker.id, !isVisible) } + color={currentColor} > {isEditing ? ( + + +
+
+ Color +
+ + + + + + setShowDeleteDialog(true)} diff --git a/src/server/models/Map.ts b/src/server/models/Map.ts index d73e8f63..501d77d6 100644 --- a/src/server/models/Map.ts +++ b/src/server/models/Map.ts @@ -11,6 +11,7 @@ export const mapConfigSchema = z.object({ membersDataSourceId: z.string().nullish(), markerDisplayModes: z.record(z.nativeEnum(MarkerDisplayMode)).optional(), markerColors: z.record(z.string()).optional(), + placedMarkerColors: z.record(z.string()).optional(), }); export type MapConfig = z.infer; diff --git a/src/server/trpc/routers/map.ts b/src/server/trpc/routers/map.ts index 74b08b01..fea19c7a 100644 --- a/src/server/trpc/routers/map.ts +++ b/src/server/trpc/routers/map.ts @@ -127,6 +127,7 @@ export const mapRouter = router({ membersDataSourceId: mapConfig.membersDataSourceId, markerDisplayModes: mapConfig.markerDisplayModes, markerColors: mapConfig.markerColors, + placedMarkerColors: mapConfig.placedMarkerColors, } as z.infer; return updateMap(mapId, { config }); From a649bd659a4952491d83197cba89db486c837ba1 Mon Sep 17 00:00:00 2001 From: Arbyhisenaj <41119392+Arbyhisenaj@users.noreply.github.com> Date: Wed, 21 Jan 2026 11:34:12 +0000 Subject: [PATCH 6/8] folder colours and drag preview --- .../MarkersControl/MarkerDragOverlay.tsx | 18 ++++--- .../controls/MarkersControl/MarkersList.tsx | 51 ++++++++++++++++-- .../MarkersControl/SortableFolderItem.tsx | 53 ++++++++++++++++++- src/server/models/Map.ts | 1 + src/server/trpc/routers/map.ts | 1 + 5 files changed, 114 insertions(+), 10 deletions(-) diff --git a/src/app/map/[id]/components/controls/MarkersControl/MarkerDragOverlay.tsx b/src/app/map/[id]/components/controls/MarkersControl/MarkerDragOverlay.tsx index 5563a892..d0d648e4 100644 --- a/src/app/map/[id]/components/controls/MarkersControl/MarkerDragOverlay.tsx +++ b/src/app/map/[id]/components/controls/MarkersControl/MarkerDragOverlay.tsx @@ -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 ( -
+
- - {marker.label} - +
+ + {marker.label} + +
); } diff --git a/src/app/map/[id]/components/controls/MarkersControl/MarkersList.tsx b/src/app/map/[id]/components/controls/MarkersControl/MarkersList.tsx index 71200ed1..8d7a4e42 100644 --- a/src/app/map/[id]/components/controls/MarkersControl/MarkersList.tsx +++ b/src/app/map/[id]/components/controls/MarkersControl/MarkersList.tsx @@ -40,6 +40,8 @@ import { } from "@/app/map/[id]/utils/position"; import { useTRPC } from "@/services/trpc/react"; import { LayerType } from "@/types"; +import { mapColors } from "../../../styles"; +import { useMapConfig } from "../../../hooks/useMapConfig"; import { useMapId } from "../../../hooks/useMapCore"; import DataSourceControl from "../DataSourceItem"; import EmptyLayer from "../LayerEmptyMessage"; @@ -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(); @@ -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") { @@ -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-", ""); @@ -325,10 +340,23 @@ 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( @@ -397,13 +425,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 || @@ -493,7 +535,10 @@ export default function MarkersList({ {createPortal( {activeId && getActiveMarker() && ( - + )} , document.body, diff --git a/src/app/map/[id]/components/controls/MarkersControl/SortableFolderItem.tsx b/src/app/map/[id]/components/controls/MarkersControl/SortableFolderItem.tsx index 58ffbe6b..19e1d991 100644 --- a/src/app/map/[id]/components/controls/MarkersControl/SortableFolderItem.tsx +++ b/src/app/map/[id]/components/controls/MarkersControl/SortableFolderItem.tsx @@ -15,19 +15,29 @@ import { TrashIcon, } from "lucide-react"; import { useMemo, useState } from "react"; +import ColorPalette from "@/components/ColorPalette"; import { sortByPositionAndId } from "@/app/map/[id]/utils/position"; import DeleteConfirmationDialog from "@/components/DeleteConfirmationDialog"; +import { mapColors } from "../../../styles"; import { ContextMenu, ContextMenuContent, ContextMenuItem, + ContextMenuLabel, ContextMenuSeparator, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, ContextMenuTrigger, } from "@/shadcn/ui/context-menu"; import { cn } from "@/shadcn/utils"; import { LayerType } from "@/types"; +import { useMapConfig } from "../../../hooks/useMapConfig"; import { useFolderMutations } from "../../../hooks/useFolders"; -import { usePlacedMarkerState } from "../../../hooks/usePlacedMarkers"; +import { + usePlacedMarkerMutations, + usePlacedMarkerState, +} from "../../../hooks/usePlacedMarkers"; import ControlEditForm from "../ControlEditForm"; import ControlWrapper from "../ControlWrapper"; import SortableMarkerItem from "./SortableMarkerItem"; @@ -78,6 +88,28 @@ export default function SortableFolderItem({ usePlacedMarkerState(); const { updateFolder, deleteFolder } = useFolderMutations(); + const { mapConfig, updateMapConfig } = useMapConfig(); + const { updatePlacedMarker } = usePlacedMarkerMutations(); + + // Get current folder color (defaults to marker color) + const currentFolderColor = + mapConfig.folderColors?.[folder.id] ?? mapColors.markers.color; + + const handleFolderColorChange = (color: string) => { + // Update folder color and all marker colors in one operation + const updatedMarkerColors = { ...mapConfig.placedMarkerColors }; + markers.forEach((marker) => { + updatedMarkerColors[marker.id] = color; + }); + + updateMapConfig({ + folderColors: { + ...mapConfig.folderColors, + [folder.id]: color, + }, + placedMarkerColors: updatedMarkerColors, + }); + }; const [isExpanded, setExpanded] = useState(false); const [isEditing, setEditing] = useState(false); @@ -135,6 +167,7 @@ export default function SortableFolderItem({ layerType={LayerType.Marker} isVisible={isFolderVisible} onVisibilityToggle={() => onVisibilityToggle()} + color={currentFolderColor} > {isEditing ? ( + + +
+
+ Color +
+ + + + + + setShowDeleteDialog(true)} diff --git a/src/server/models/Map.ts b/src/server/models/Map.ts index 501d77d6..a5031c4a 100644 --- a/src/server/models/Map.ts +++ b/src/server/models/Map.ts @@ -12,6 +12,7 @@ export const mapConfigSchema = z.object({ markerDisplayModes: z.record(z.nativeEnum(MarkerDisplayMode)).optional(), markerColors: z.record(z.string()).optional(), placedMarkerColors: z.record(z.string()).optional(), + folderColors: z.record(z.string()).optional(), }); export type MapConfig = z.infer; diff --git a/src/server/trpc/routers/map.ts b/src/server/trpc/routers/map.ts index fea19c7a..cef5e10b 100644 --- a/src/server/trpc/routers/map.ts +++ b/src/server/trpc/routers/map.ts @@ -128,6 +128,7 @@ export const mapRouter = router({ markerDisplayModes: mapConfig.markerDisplayModes, markerColors: mapConfig.markerColors, placedMarkerColors: mapConfig.placedMarkerColors, + folderColors: mapConfig.folderColors, } as z.infer; return updateMap(mapId, { config }); From 328f1fd7cb58ace8b81deca8bef1f9e4bcc72561 Mon Sep 17 00:00:00 2001 From: Arbyhisenaj <41119392+Arbyhisenaj@users.noreply.github.com> Date: Wed, 21 Jan 2026 11:37:13 +0000 Subject: [PATCH 7/8] rearrange display as order --- .../components/controls/DataSourceItem.tsx | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/app/map/[id]/components/controls/DataSourceItem.tsx b/src/app/map/[id]/components/controls/DataSourceItem.tsx index 10866acf..10c49362 100644 --- a/src/app/map/[id]/components/controls/DataSourceItem.tsx +++ b/src/app/map/[id]/components/controls/DataSourceItem.tsx @@ -250,6 +250,24 @@ export default function DataSourceItem({ )} + + +
+
+ Color +
+ + + + + + Display as - - -
-
- Color -
- - - - - - setShowRemoveDialog(true)} From c6156333c086943e61a67159931807e7bbe4cac7 Mon Sep 17 00:00:00 2001 From: Arbyhisenaj <41119392+Arbyhisenaj@users.noreply.github.com> Date: Wed, 21 Jan 2026 15:30:12 +0000 Subject: [PATCH 8/8] lint fix --- src/app/map/[id]/components/Markers.tsx | 35 ++++--------------- .../components/controls/DataSourceItem.tsx | 3 +- .../MarkersControl/MarkerDragOverlay.tsx | 2 +- .../controls/MarkersControl/MarkersList.tsx | 12 +++++-- .../MarkersControl/SortableFolderItem.tsx | 13 +++---- .../MarkersControl/SortableMarkerItem.tsx | 3 +- src/components/ColorPalette.tsx | 9 ++--- 7 files changed, 24 insertions(+), 53 deletions(-) diff --git a/src/app/map/[id]/components/Markers.tsx b/src/app/map/[id]/components/Markers.tsx index 08c7eb18..07eaa360 100644 --- a/src/app/map/[id]/components/Markers.tsx +++ b/src/app/map/[id]/components/Markers.tsx @@ -136,13 +136,13 @@ function DataSourceMarkers({ publicMap?.id && colorScheme ? publicMapColorSchemes[colorScheme]?.primary : ""; - + // Get custom color from mapConfig, or fall back to defaults const customColor = mapConfig.markerColors?.[dataSourceMarkers.dataSourceId]; const defaultColor = isMembers ? mapColors.member.color : mapColors.dataSource.color; - + const color = publicMapColor || customColor || defaultColor; return ( @@ -228,12 +228,7 @@ function DataSourceMarkers({ source={sourceId} paint={{ // Adjust weight based on matched_count - "heatmap-weight": [ - "case", - NOT_MATCHED_CASE, - 0.5, - 1.5, - ], + "heatmap-weight": ["case", NOT_MATCHED_CASE, 0.5, 1.5], // Increase intensity as zoom level increases "heatmap-intensity": [ "interpolate", @@ -288,15 +283,7 @@ function DataSourceMarkers({ ["==", ["get", "point_count"], 1], ]} paint={{ - "circle-radius": [ - "interpolate", - ["linear"], - ["zoom"], - 8, - 3, - 16, - 8, - ], + "circle-radius": ["interpolate", ["linear"], ["zoom"], 8, 3, 16, 8], "circle-color": color, "circle-opacity": ["case", NOT_MATCHED_CASE, 0.5, 1], "circle-stroke-width": 1, @@ -319,12 +306,7 @@ function DataSourceMarkers({ "text-field": [ "concat", ["slice", ["get", "name"], 0, 20], - [ - "case", - [">", ["length", ["get", "name"]], 20], - "...", - "", - ], + ["case", [">", ["length", ["get", "name"]], 20], "...", ""], ], "text-font": ["DIN Pro Medium", "Arial Unicode MS Bold"], "text-size": 12, @@ -371,12 +353,7 @@ function DataSourceMarkers({ "text-field": [ "concat", ["slice", ["get", "name"], 0, 20], - [ - "case", - [">", ["length", ["get", "name"]], 20], - "...", - "", - ], + ["case", [">", ["length", ["get", "name"]], 20], "...", ""], ], "text-font": ["DIN Pro Medium", "Arial Unicode MS Bold"], "text-size": 12, diff --git a/src/app/map/[id]/components/controls/DataSourceItem.tsx b/src/app/map/[id]/components/controls/DataSourceItem.tsx index 10c49362..19c2f6aa 100644 --- a/src/app/map/[id]/components/controls/DataSourceItem.tsx +++ b/src/app/map/[id]/components/controls/DataSourceItem.tsx @@ -76,8 +76,7 @@ export default function DataSourceItem({ mapConfig.markerDisplayModes?.[dataSource.id] ?? MarkerDisplayMode.Clusters; // Get current color (defaults to layer color) - const currentColor = - mapConfig.markerColors?.[dataSource.id] ?? layerColor; + const currentColor = mapConfig.markerColors?.[dataSource.id] ?? layerColor; const handleDisplayModeChange = (mode: MarkerDisplayMode) => { updateMapConfig({ diff --git a/src/app/map/[id]/components/controls/MarkersControl/MarkerDragOverlay.tsx b/src/app/map/[id]/components/controls/MarkersControl/MarkerDragOverlay.tsx index d0d648e4..729d331a 100644 --- a/src/app/map/[id]/components/controls/MarkersControl/MarkerDragOverlay.tsx +++ b/src/app/map/[id]/components/controls/MarkersControl/MarkerDragOverlay.tsx @@ -9,7 +9,7 @@ export default function MarkerDragOverlay({ color?: string; }) { const markerColor = color || mapColors.markers.color; - + return (
{ 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]; diff --git a/src/app/map/[id]/components/controls/MarkersControl/SortableFolderItem.tsx b/src/app/map/[id]/components/controls/MarkersControl/SortableFolderItem.tsx index 19e1d991..f897a9e2 100644 --- a/src/app/map/[id]/components/controls/MarkersControl/SortableFolderItem.tsx +++ b/src/app/map/[id]/components/controls/MarkersControl/SortableFolderItem.tsx @@ -15,15 +15,13 @@ import { TrashIcon, } from "lucide-react"; import { useMemo, useState } from "react"; -import ColorPalette from "@/components/ColorPalette"; import { sortByPositionAndId } from "@/app/map/[id]/utils/position"; +import ColorPalette from "@/components/ColorPalette"; import DeleteConfirmationDialog from "@/components/DeleteConfirmationDialog"; -import { mapColors } from "../../../styles"; import { ContextMenu, ContextMenuContent, ContextMenuItem, - ContextMenuLabel, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, @@ -32,12 +30,10 @@ import { } from "@/shadcn/ui/context-menu"; import { cn } from "@/shadcn/utils"; import { LayerType } from "@/types"; -import { useMapConfig } from "../../../hooks/useMapConfig"; import { useFolderMutations } from "../../../hooks/useFolders"; -import { - usePlacedMarkerMutations, - usePlacedMarkerState, -} from "../../../hooks/usePlacedMarkers"; +import { useMapConfig } from "../../../hooks/useMapConfig"; +import { usePlacedMarkerState } from "../../../hooks/usePlacedMarkers"; +import { mapColors } from "../../../styles"; import ControlEditForm from "../ControlEditForm"; import ControlWrapper from "../ControlWrapper"; import SortableMarkerItem from "./SortableMarkerItem"; @@ -89,7 +85,6 @@ export default function SortableFolderItem({ const { updateFolder, deleteFolder } = useFolderMutations(); const { mapConfig, updateMapConfig } = useMapConfig(); - const { updatePlacedMarker } = usePlacedMarkerMutations(); // Get current folder color (defaults to marker color) const currentFolderColor = diff --git a/src/app/map/[id]/components/controls/MarkersControl/SortableMarkerItem.tsx b/src/app/map/[id]/components/controls/MarkersControl/SortableMarkerItem.tsx index e901f28a..bb01ddb2 100644 --- a/src/app/map/[id]/components/controls/MarkersControl/SortableMarkerItem.tsx +++ b/src/app/map/[id]/components/controls/MarkersControl/SortableMarkerItem.tsx @@ -7,12 +7,10 @@ import { useEffect, useState } from "react"; import { toast } from "sonner"; import ColorPalette from "@/components/ColorPalette"; import DeleteConfirmationDialog from "@/components/DeleteConfirmationDialog"; -import { mapColors } from "../../../styles"; import { ContextMenu, ContextMenuContent, ContextMenuItem, - ContextMenuLabel, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, @@ -26,6 +24,7 @@ import { usePlacedMarkerMutations, usePlacedMarkerState, } from "../../../hooks/usePlacedMarkers"; +import { mapColors } from "../../../styles"; import ControlEditForm from "../ControlEditForm"; import ControlWrapper from "../ControlWrapper"; import type { PlacedMarker } from "@/server/models/PlacedMarker"; diff --git a/src/components/ColorPalette.tsx b/src/components/ColorPalette.tsx index 63a161d7..39394fbb 100644 --- a/src/components/ColorPalette.tsx +++ b/src/components/ColorPalette.tsx @@ -32,12 +32,7 @@ export default function ColorPalette({ className, }: ColorPaletteProps) { return ( -
+
{colors.map((color) => (